bunker-admin 39d74e7b85 Add guided tour, media enhancements, error handling, and DevOps improvements
Major additions: onboarding tour system, correlation-id middleware, media
error handler, restore script, env validation script, Dockerignore files.
Updates across 70+ admin components for improved UX and error handling.

Bunker Admin
2026-03-26 10:31:51 -06:00

1068 lines
55 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { PrismaClient, UserRole, EmailTemplateCategory } from '@prisma/client';
import bcrypt from 'bcryptjs';
import * as fs from 'fs';
import * as path from 'path';
import { env } from '../src/config/env';
import { initEncryption, encrypt } from '../src/utils/crypto';
const prisma = new PrismaClient();
async function main() {
console.log('Seeding database...');
// Create default super admin from environment variables
// Skip creation if password is a placeholder (contains "CHANGE_THIS")
const initialAdminEmail = env.INITIAL_ADMIN_EMAIL;
const initialAdminPassword = env.INITIAL_ADMIN_PASSWORD;
let admin: { id: string; email: string } | null = null;
// Validate password meets policy before proceeding
if (initialAdminPassword.length < 12 ||
!/[A-Z]/.test(initialAdminPassword) ||
!/[a-z]/.test(initialAdminPassword) ||
!/[0-9]/.test(initialAdminPassword)) {
console.warn('⚠️ INITIAL_ADMIN_PASSWORD does not meet password policy (12+ chars, uppercase, lowercase, digit)');
console.warn('⚠️ Skipping admin user creation. Please set a valid password in .env');
} else if (initialAdminPassword.includes('CHANGE_THIS') || initialAdminPassword.includes('REQUIRED')) {
console.warn('⚠️ INITIAL_ADMIN_PASSWORD contains placeholder value');
console.warn('⚠️ Skipping admin user creation. Please set a real password in .env');
} else {
const hashedPassword = await bcrypt.hash(initialAdminPassword, 12);
admin = await prisma.user.upsert({
where: { email: initialAdminEmail },
update: {
password: hashedPassword,
emailVerified: true,
status: 'ACTIVE',
},
create: {
email: initialAdminEmail,
password: hashedPassword,
name: 'Admin',
role: UserRole.SUPER_ADMIN,
roles: JSON.parse(JSON.stringify([UserRole.SUPER_ADMIN])),
emailVerified: true,
},
});
console.log(`✅ Created/updated admin user: ${admin.email}`);
}
// If admin wasn't created (placeholder password), try to find existing admin
if (!admin) {
admin = await prisma.user.findFirst({
where: { role: UserRole.SUPER_ADMIN },
select: { id: true, email: true },
});
if (admin) {
console.log(` Found existing admin user: ${admin.email}`);
} else {
console.error('❌ FATAL: No SUPER_ADMIN user exists and none could be created.');
console.error(' Fix INITIAL_ADMIN_PASSWORD in .env (12+ chars, uppercase, lowercase, digit)');
process.exit(2);
}
}
// Create default map settings
await prisma.mapSettings.upsert({
where: { id: 'default' },
update: {},
create: {
id: 'default',
latitude: 53.5461,
longitude: -113.4938,
zoom: 11,
walkSheetTitle: 'Walk Sheet',
walkSheetSubtitle: '',
walkSheetFooter: '',
},
});
console.log('Created default map settings');
// Seed SiteSettings with .env SMTP values (only if no row exists yet)
const existingSettings = await prisma.siteSettings.findFirst();
if (!existingSettings) {
// Initialize encryption so we can encrypt the SMTP password
const encryptionKey = env.ENCRYPTION_KEY || env.JWT_ACCESS_SECRET;
initEncryption(encryptionKey);
const isMailhog = env.EMAIL_TEST_MODE === 'true' || env.SMTP_HOST === 'mailhog-changemaker';
await prisma.siteSettings.create({
data: {
smtpHost: env.SMTP_HOST,
smtpPort: env.SMTP_PORT,
smtpUser: env.SMTP_USER,
smtpPass: env.SMTP_PASS ? encrypt(env.SMTP_PASS) : '',
smtpFromAddress: env.SMTP_FROM,
emailFromName: env.SMTP_FROM_NAME,
smtpActiveProvider: isMailhog ? 'mailhog' : 'production',
emailTestMode: env.EMAIL_TEST_MODE === 'true',
testEmailRecipient: env.TEST_EMAIL_RECIPIENT,
navConfig: {
items: [
{ id: 'home', label: 'Home', path: '/', icon: 'HomeOutlined', enabled: true, order: 0, type: 'builtin', external: true },
{ id: 'campaigns', label: 'Campaigns', path: '/campaigns', icon: 'SendOutlined', enabled: true, order: 1, type: 'builtin', featureFlag: 'enableInfluence' },
{ id: 'map', label: 'Map', path: '/map', icon: 'EnvironmentOutlined', enabled: true, order: 2, type: 'builtin', featureFlag: 'enableMap' },
{ id: 'shifts', label: 'Shifts', path: '/shifts', icon: 'CalendarOutlined', enabled: true, order: 3, type: 'builtin', featureFlag: 'enableMap' },
{ id: 'events', label: 'Events', path: '/events', icon: 'CalendarOutlined', enabled: true, order: 4, type: 'builtin', featureFlag: 'enableEvents', external: true },
{ id: 'gallery', label: 'Gallery', path: '/gallery', icon: 'PlayCircleOutlined', enabled: true, order: 5, type: 'builtin', featureFlag: 'enableMediaFeatures' },
{ id: 'pricing', label: 'Pricing', path: '/pricing', icon: 'DollarOutlined', enabled: true, order: 6, type: 'builtin', featureFlag: 'enablePayments' },
{ id: 'shop', label: 'Shop', path: '/shop', icon: 'ShoppingOutlined', enabled: true, order: 7, type: 'builtin', featureFlag: 'enablePayments' },
{ id: 'donate', label: 'Donate', path: '/donate', icon: 'HeartOutlined', enabled: true, order: 8, type: 'builtin', featureFlag: 'enablePayments' },
{ id: 'landing', label: 'Website', path: '$landing', icon: 'GlobalOutlined', enabled: false, order: 9, type: 'builtin', external: true },
{ id: 'docs', label: 'Docs', path: '$docs', icon: 'BookOutlined', enabled: false, order: 10, type: 'builtin', external: true },
],
},
},
});
console.log('Created SiteSettings with SMTP config + navConfig from .env');
} else {
console.log('SiteSettings already exists, skipping SMTP seeding');
// Seed navConfig if null (existing installations)
if (!existingSettings.navConfig) {
const defaultNavConfig = {
items: [
{ id: 'home', label: 'Home', path: '/', icon: 'HomeOutlined', enabled: true, order: 0, type: 'builtin', external: true },
{ id: 'campaigns', label: 'Campaigns', path: '/campaigns', icon: 'SendOutlined', enabled: true, order: 1, type: 'builtin', featureFlag: 'enableInfluence' },
{ id: 'map', label: 'Map', path: '/map', icon: 'EnvironmentOutlined', enabled: true, order: 2, type: 'builtin', featureFlag: 'enableMap' },
{ id: 'shifts', label: 'Shifts', path: '/shifts', icon: 'CalendarOutlined', enabled: true, order: 3, type: 'builtin', featureFlag: 'enableMap' },
{ id: 'events', label: 'Events', path: '/events', icon: 'CalendarOutlined', enabled: true, order: 4, type: 'builtin', featureFlag: 'enableEvents', external: true },
{ id: 'gallery', label: 'Gallery', path: '/gallery', icon: 'PlayCircleOutlined', enabled: true, order: 5, type: 'builtin', featureFlag: 'enableMediaFeatures' },
{ id: 'pricing', label: 'Pricing', path: '/pricing', icon: 'DollarOutlined', enabled: true, order: 6, type: 'builtin', featureFlag: 'enablePayments' },
{ id: 'shop', label: 'Shop', path: '/shop', icon: 'ShoppingOutlined', enabled: true, order: 7, type: 'builtin', featureFlag: 'enablePayments' },
{ id: 'donate', label: 'Donate', path: '/donate', icon: 'HeartOutlined', enabled: true, order: 8, type: 'builtin', featureFlag: 'enablePayments' },
{ id: 'landing', label: 'Website', path: '$landing', icon: 'GlobalOutlined', enabled: false, order: 9, type: 'builtin', external: true },
{ id: 'docs', label: 'Docs', path: '$docs', icon: 'BookOutlined', enabled: false, order: 10, type: 'builtin', external: true },
],
};
await prisma.siteSettings.update({
where: { id: existingSettings.id },
data: { navConfig: defaultNavConfig },
});
console.log('Seeded default navConfig on existing SiteSettings');
}
}
// Create default page blocks for landing page builder
const defaultBlocks = [
{
id: 'default-hero',
type: 'hero',
label: 'Hero Section',
category: 'Headers',
sortOrder: 1,
schema: {
title: { type: 'string', label: 'Title' },
subtitle: { type: 'string', label: 'Subtitle' },
backgroundImage: { type: 'string', label: 'Background Image URL' },
ctaText: { type: 'string', label: 'Button Text' },
ctaUrl: { type: 'string', label: 'Button URL' },
},
defaults: {
title: 'Welcome to Our Campaign',
subtitle: 'Join us in making a difference in your community.',
backgroundImage: '',
ctaText: 'Get Involved',
ctaUrl: '#',
},
},
{
id: 'default-text',
type: 'text',
label: 'Text Block',
category: 'Content',
sortOrder: 2,
schema: {
heading: { type: 'string', label: 'Heading' },
body: { type: 'text', label: 'Body Text' },
},
defaults: {
heading: 'About Us',
body: 'Tell your story here. Explain your mission, values, and what drives your campaign forward.',
},
},
{
id: 'default-features',
type: 'features',
label: 'Features Grid',
category: 'Content',
sortOrder: 3,
schema: {
features: { type: 'array', label: 'Features', items: { title: 'string', description: 'string', icon: 'string' } },
},
defaults: {
features: [
{ title: 'Community Action', description: 'Organize local events and initiatives.', icon: '' },
{ title: 'Advocacy', description: 'Email your representatives directly.', icon: '' },
{ title: 'Volunteer', description: 'Sign up for shifts and make a difference.', icon: '' },
],
},
},
{
id: 'default-cta',
type: 'cta',
label: 'Call to Action',
category: 'Actions',
sortOrder: 4,
schema: {
heading: { type: 'string', label: 'Heading' },
description: { type: 'string', label: 'Description' },
buttonText: { type: 'string', label: 'Button Text' },
buttonUrl: { type: 'string', label: 'Button URL' },
},
defaults: {
heading: 'Ready to Take Action?',
description: 'Join thousands of community members making their voices heard.',
buttonText: 'Join Now',
buttonUrl: '#',
},
},
{
id: 'default-testimonials',
type: 'testimonials',
label: 'Testimonials',
category: 'Content',
sortOrder: 5,
schema: {
quotes: { type: 'array', label: 'Quotes', items: { text: 'string', author: 'string', role: 'string' } },
},
defaults: {
quotes: [
{ text: 'This platform made it so easy to contact my representatives.', author: 'Jane D.', role: 'Community Member' },
{ text: 'I signed up for a volunteer shift and it changed my perspective.', author: 'Mark S.', role: 'Volunteer' },
],
},
},
{
id: 'default-contact-form',
type: 'contact-form',
label: 'Contact Form',
category: 'Actions',
sortOrder: 6,
schema: {
heading: { type: 'string', label: 'Heading' },
fields: { type: 'array', label: 'Fields', items: { name: 'string', type: 'string', required: 'boolean' } },
},
defaults: {
heading: 'Get in Touch',
fields: [
{ name: 'Name', type: 'text', required: true },
{ name: 'Email', type: 'email', required: true },
{ name: 'Message', type: 'textarea', required: true },
],
},
},
{
id: 'default-video',
type: 'video',
label: 'Video Player',
category: 'Media',
sortOrder: 7,
schema: {
videoId: { type: 'number', label: 'Video ID', required: true },
playerType: { type: 'select', label: 'Player Type', options: ['standard', 'advanced'], default: 'standard' },
width: { type: 'string', label: 'Width', default: '100%' },
height: { type: 'string', label: 'Height', default: 'auto' },
autoplay: { type: 'boolean', label: 'Autoplay', default: false },
controls: { type: 'boolean', label: 'Show Controls', default: true },
showReactions: { type: 'boolean', label: 'Show Reactions (Advanced Only)', default: true },
},
defaults: {
videoId: null,
playerType: 'standard',
width: '100%',
height: 'auto',
autoplay: false,
controls: true,
showReactions: true,
},
},
{
id: 'default-video-card',
type: 'video-card',
label: 'Video Card',
category: 'Media',
sortOrder: 8,
schema: {
videoId: { type: 'number', label: 'Video ID', required: true },
title: { type: 'string', label: 'Title' },
durationSeconds: { type: 'number', label: 'Duration (seconds)', default: 0 },
quality: { type: 'string', label: 'Quality (e.g. 1080p)' },
viewCount: { type: 'number', label: 'View Count', default: 0 },
},
defaults: {
videoId: null,
title: '',
durationSeconds: 0,
quality: '',
viewCount: 0,
},
},
{
id: 'default-donate-button',
type: 'donate-button',
label: 'Donate Button',
category: 'Payments',
sortOrder: 9,
schema: {
buttonText: { type: 'string', label: 'Button Text', default: 'Donate Now' },
showAmounts: { type: 'boolean', label: 'Show Suggested Amounts', default: true },
heading: { type: 'string', label: 'Heading', default: 'Support Our Cause' },
description: { type: 'string', label: 'Description' },
},
defaults: {
buttonText: 'Donate Now',
showAmounts: true,
heading: 'Support Our Cause',
description: 'Your contribution helps us create lasting change in our community.',
},
},
{
id: 'default-pricing-table',
type: 'pricing-table',
label: 'Pricing Table',
category: 'Payments',
sortOrder: 10,
schema: {
showYearly: { type: 'boolean', label: 'Show Yearly Toggle', default: true },
heading: { type: 'string', label: 'Heading', default: 'Choose Your Plan' },
description: { type: 'string', label: 'Description' },
},
defaults: {
showYearly: true,
heading: 'Choose Your Plan',
description: 'Get access to exclusive content and features.',
},
},
{
id: 'default-product-card',
type: 'product-card',
label: 'Product Card',
category: 'Payments',
sortOrder: 11,
schema: {
productSlug: { type: 'string', label: 'Product Slug', required: true },
buttonText: { type: 'string', label: 'Button Text', default: 'Buy Now' },
},
defaults: {
productSlug: '',
buttonText: 'Buy Now',
},
},
{
id: 'default-campaign-form',
type: 'campaign-form',
label: 'Campaign Email Form',
category: 'Influence',
sortOrder: 13,
schema: {
campaignSlug: { type: 'string', label: 'Campaign Slug', required: true },
compact: { type: 'boolean', label: 'Compact Mode', default: false },
},
defaults: {
campaignSlug: '',
compact: false,
},
},
{
id: 'default-gancio-events',
type: 'gancio-events',
label: 'Event Calendar',
category: 'Content',
sortOrder: 12,
schema: {
maxlength: { type: 'number', label: 'Max Events', default: 10 },
theme: { type: 'select', label: 'Theme', options: ['dark', 'light'], default: 'dark' },
tags: { type: 'string', label: 'Filter by Tags (comma-separated)' },
title: { type: 'string', label: 'Section Title', default: 'Upcoming Events' },
},
defaults: {
maxlength: 10,
theme: 'dark',
tags: '',
title: 'Upcoming Events',
},
},
{
id: 'default-photo',
type: 'photo',
label: 'Photo',
category: 'Media',
sortOrder: 14,
schema: {
photoId: { type: 'number', label: 'Photo ID', required: true },
size: { type: 'select', label: 'Size', options: ['thumb', 'medium', 'large'], default: 'large' },
caption: { type: 'string', label: 'Caption' },
linkToGallery: { type: 'boolean', label: 'Link to Gallery', default: true },
alignment: { type: 'select', label: 'Alignment', options: ['left', 'center', 'right'], default: 'center' },
maxWidth: { type: 'string', label: 'Max Width', default: '100%' },
},
defaults: {
photoId: null,
size: 'large',
caption: '',
linkToGallery: true,
alignment: 'center',
maxWidth: '100%',
},
},
{
id: 'default-photo-card',
type: 'photo-card',
label: 'Photo Card',
category: 'Media',
sortOrder: 15,
schema: {
photoId: { type: 'number', label: 'Photo ID', required: true },
title: { type: 'string', label: 'Title' },
description: { type: 'string', label: 'Description' },
showMetadata: { type: 'boolean', label: 'Show Metadata (format, dimensions)', default: true },
},
defaults: {
photoId: null,
title: '',
description: '',
showMetadata: true,
},
},
{
id: 'default-photo-album',
type: 'photo-album',
label: 'Photo Album',
category: 'Media',
sortOrder: 16,
schema: {
albumId: { type: 'number', label: 'Album ID', required: true },
columns: { type: 'select', label: 'Columns', options: ['2', '3', '4'], default: '3' },
maxPhotos: { type: 'number', label: 'Max Photos', default: 12 },
showTitle: { type: 'boolean', label: 'Show Album Title', default: true },
},
defaults: {
albumId: null,
columns: '3',
maxPhotos: 12,
showTitle: true,
},
},
{
id: 'default-scheduling-poll',
type: 'scheduling-poll',
label: 'Scheduling Poll',
category: 'Influence',
sortOrder: 17,
schema: {
pollSlug: { type: 'string', label: 'Poll Slug', required: true },
showComments: { type: 'boolean', label: 'Show Comments', default: true },
title: { type: 'string', label: 'Section Title', default: 'Vote on a Meeting Time' },
},
defaults: {
pollSlug: '',
showComments: true,
title: 'Vote on a Meeting Time',
},
},
];
for (const block of defaultBlocks) {
await prisma.pageBlock.upsert({
where: { id: block.id },
update: {},
create: block,
});
}
console.log(`Created ${defaultBlocks.length} default page blocks`);
// Seed email templates (skip if no admin user)
if (admin) {
await seedEmailTemplates(admin);
} else {
console.warn('⚠️ No admin user found - skipping email template seeding');
}
// Seed SMS notification templates
await seedSmsNotificationTemplates();
// Seed pre-made gallery ads (all inactive by default — admin enables manually)
await seedGalleryAds();
console.log('Seed complete.');
}
/**
* Seed email templates from filesystem
*/
async function seedEmailTemplates(admin: { id: string; email: string }) {
console.log('Seeding email templates...');
const templatesDir = path.join(__dirname, '../src/templates/email');
const templateDefinitions = [
{
key: 'campaign-email',
name: 'Campaign Email to Representative',
description: 'Email sent when a constituent contacts their elected representative through a campaign',
category: EmailTemplateCategory.INFLUENCE,
subjectLine: '{{CAMPAIGN_TITLE}} - Message from {{USER_NAME}}',
isSystem: true,
variables: [
{ key: 'CAMPAIGN_TITLE', label: 'Campaign Title', description: 'Title of the advocacy campaign', isRequired: true, isConditional: false, sampleValue: 'Support Climate Action Bill C-12', sortOrder: 0 },
{ key: 'MESSAGE', label: 'Message Body', description: 'The message content written by the constituent', isRequired: true, isConditional: false, sampleValue: 'I urge you to support this important legislation...', sortOrder: 1 },
{ key: 'USER_NAME', label: 'Sender Name', description: 'Name of the constituent sending the email', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 2 },
{ key: 'USER_EMAIL', label: 'Sender Email', description: 'Email address of the constituent', isRequired: true, isConditional: false, sampleValue: 'jane@example.com', sortOrder: 3 },
{ key: 'POSTAL_CODE', label: 'Postal Code', description: 'Postal code of the constituent', isRequired: true, isConditional: false, sampleValue: 'K1A 0B1', sortOrder: 4 },
{ key: 'RECIPIENT_NAME', label: 'Representative Name', description: 'Name of the representative receiving the email', isRequired: false, isConditional: true, sampleValue: 'Hon. John Smith', sortOrder: 5 },
{ key: 'RECIPIENT_LEVEL', label: 'Government Level', description: 'Level of government', isRequired: false, isConditional: true, sampleValue: 'Federal', sortOrder: 6 },
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 7 },
{ key: 'TIMESTAMP', label: 'Timestamp', description: 'Date and time the email was sent', isRequired: true, isConditional: false, sampleValue: '2025-02-11T10:30:00Z', sortOrder: 8 },
],
},
{
key: 'response-verification',
name: 'Response Verification Email',
description: 'Email sent to representatives to verify a submitted response',
category: EmailTemplateCategory.INFLUENCE,
subjectLine: 'Verify Your Response - {{CAMPAIGN_TITLE}}',
isSystem: true,
variables: [
{ key: 'CAMPAIGN_TITLE', label: 'Campaign Title', description: 'Title of the advocacy campaign', isRequired: true, isConditional: false, sampleValue: 'Support Climate Action Bill C-12', sortOrder: 0 },
{ key: 'RESPONSE_TYPE', label: 'Response Type', description: 'Type of response', isRequired: true, isConditional: false, sampleValue: 'SUPPORT', sortOrder: 1 },
{ key: 'RESPONSE_TEXT', label: 'Response Text', description: 'The text of the response submitted', isRequired: true, isConditional: false, sampleValue: 'I fully support this initiative...', sortOrder: 2 },
{ key: 'SUBMITTER_NAME', label: 'Submitter Name', description: 'Name of the person who submitted the response', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 3 },
{ key: 'SUBMITTED_DATE', label: 'Submitted Date', description: 'Date the response was submitted', isRequired: true, isConditional: false, sampleValue: 'February 11, 2025', sortOrder: 4 },
{ key: 'VERIFICATION_URL', label: 'Verification URL', description: 'URL to verify the response', isRequired: true, isConditional: false, sampleValue: 'https://api.cmlite.org/api/responses/123/verify/abc123', sortOrder: 5 },
{ key: 'REPORT_URL', label: 'Report URL', description: 'URL to report the response as invalid', isRequired: true, isConditional: false, sampleValue: 'https://api.cmlite.org/api/responses/123/report/abc123', sortOrder: 6 },
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 7 },
{ key: 'TIMESTAMP', label: 'Timestamp', description: 'Date and time the verification email was sent', isRequired: true, isConditional: false, sampleValue: '2025-02-11T10:30:00Z', sortOrder: 8 },
],
},
{
key: 'shift-signup-confirmation',
name: 'Shift Signup Confirmation',
description: 'Email sent when a volunteer signs up for a shift',
category: EmailTemplateCategory.MAP,
subjectLine: 'Shift Confirmation - {{SHIFT_TITLE}}',
isSystem: true,
variables: [
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
{ key: 'USER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer', isRequired: true, isConditional: false, sampleValue: 'John Smith', sortOrder: 1 },
{ key: 'USER_EMAIL', label: 'Volunteer Email', description: 'Email address of the volunteer', isRequired: true, isConditional: false, sampleValue: 'john@example.com', sortOrder: 2 },
{ key: 'SHIFT_TITLE', label: 'Shift Title', description: 'Title of the volunteer shift', isRequired: true, isConditional: false, sampleValue: 'Weekend Canvassing - Downtown', sortOrder: 3 },
{ key: 'SHIFT_DATE', label: 'Shift Date', description: 'Date of the volunteer shift', isRequired: true, isConditional: false, sampleValue: 'Saturday, February 15, 2025', sortOrder: 4 },
{ key: 'SHIFT_TIME', label: 'Shift Time', description: 'Time of the volunteer shift', isRequired: true, isConditional: false, sampleValue: '10:00 AM — 2:00 PM', sortOrder: 5 },
{ key: 'SHIFT_LOCATION', label: 'Shift Location', description: 'Location of the volunteer shift', isRequired: true, isConditional: false, sampleValue: '123 Main Street', sortOrder: 6 },
{ key: 'IS_NEW_USER', label: 'Is New User', description: 'Whether this is a new user account', isRequired: false, isConditional: true, sampleValue: 'true', sortOrder: 7 },
{ key: 'TEMP_PASSWORD', label: 'Temporary Password', description: 'Temporary password for new user account', isRequired: false, isConditional: true, sampleValue: 'SwiftEagle42', sortOrder: 8 },
{ key: 'LOGIN_URL', label: 'Login URL', description: 'URL to log in to the system', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/login', sortOrder: 9 },
],
},
{
key: 'shift-details',
name: 'Shift Details Reminder',
description: 'Reminder email sent to all volunteers signed up for a shift',
category: EmailTemplateCategory.MAP,
subjectLine: 'Shift Reminder - {{SHIFT_TITLE}} on {{SHIFT_DATE}}',
isSystem: true,
variables: [
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
{ key: 'USER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer', isRequired: true, isConditional: false, sampleValue: 'John Smith', sortOrder: 1 },
{ key: 'SHIFT_TITLE', label: 'Shift Title', description: 'Title of the volunteer shift', isRequired: true, isConditional: false, sampleValue: 'Weekend Canvassing - Downtown', sortOrder: 2 },
{ key: 'SHIFT_DATE', label: 'Shift Date', description: 'Date of the volunteer shift', isRequired: true, isConditional: false, sampleValue: 'Saturday, February 15, 2025', sortOrder: 3 },
{ key: 'SHIFT_START_TIME', label: 'Start Time', description: 'Start time of the shift', isRequired: true, isConditional: false, sampleValue: '10:00 AM', sortOrder: 4 },
{ key: 'SHIFT_END_TIME', label: 'End Time', description: 'End time of the shift', isRequired: true, isConditional: false, sampleValue: '2:00 PM', sortOrder: 5 },
{ key: 'SHIFT_LOCATION', label: 'Shift Location', description: 'Location of the volunteer shift', isRequired: true, isConditional: false, sampleValue: '123 Main Street', sortOrder: 6 },
{ key: 'SHIFT_DESCRIPTION', label: 'Shift Description', description: 'Description of the shift', isRequired: false, isConditional: true, sampleValue: 'We will be canvassing the downtown area...', sortOrder: 7 },
{ key: 'CURRENT_VOLUNTEERS', label: 'Current Volunteers', description: 'Number of volunteers currently signed up', isRequired: true, isConditional: false, sampleValue: '8', sortOrder: 8 },
{ key: 'MAX_VOLUNTEERS', label: 'Max Volunteers', description: 'Maximum number of volunteers allowed', isRequired: true, isConditional: false, sampleValue: '10', sortOrder: 9 },
{ key: 'SHIFT_STATUS', label: 'Shift Status', description: 'Status of the shift', isRequired: true, isConditional: false, sampleValue: 'OPEN', sortOrder: 10 },
],
},
{
key: 'email-verification',
name: 'Email Verification',
description: 'Sent to new users to verify their email address after self-registration',
category: EmailTemplateCategory.SYSTEM,
subjectLine: 'Verify your email — {{ORGANIZATION_NAME}}',
isSystem: true,
variables: [
{ key: 'RECIPIENT_NAME', label: 'User Name', description: 'Name of the user being verified', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 0 },
{ key: 'VERIFICATION_URL', label: 'Verification URL', description: 'URL the user clicks to verify their email', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/verify-email?token=abc123', sortOrder: 1 },
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 2 },
{ key: 'EXPIRY_HOURS', label: 'Expiry Hours', description: 'Number of hours until the verification link expires', isRequired: true, isConditional: false, sampleValue: '24', sortOrder: 3 },
],
},
{
key: 'password-reset',
name: 'Password Reset',
description: 'Sent when a user requests a password reset link',
category: EmailTemplateCategory.SYSTEM,
subjectLine: 'Reset your password — {{ORGANIZATION_NAME}}',
isSystem: true,
variables: [
{ key: 'RECIPIENT_NAME', label: 'User Name', description: 'Name of the user requesting a reset', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 0 },
{ key: 'RESET_URL', label: 'Reset URL', description: 'URL the user clicks to reset their password', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/reset-password?token=abc123', sortOrder: 1 },
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 2 },
],
},
{
key: 'account-approved',
name: 'Account Approved',
description: 'Sent to a user when an admin approves their account after email verification',
category: EmailTemplateCategory.SYSTEM,
subjectLine: 'Account approved — {{ORGANIZATION_NAME}}',
isSystem: true,
variables: [
{ key: 'RECIPIENT_NAME', label: 'User Name', description: 'Name of the approved user', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 0 },
{ key: 'LOGIN_URL', label: 'Login URL', description: 'URL to the login page', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/login', sortOrder: 1 },
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 2 },
],
},
{
key: 'account-pending-approval',
name: 'Account Pending Approval (Admin Notification)',
description: 'Sent to admins when a new user verifies their email and is awaiting approval',
category: EmailTemplateCategory.SYSTEM,
subjectLine: 'New user awaiting approval — {{ORGANIZATION_NAME}}',
isSystem: true,
variables: [
{ key: 'NEW_USER_NAME', label: 'New User Name', description: 'Name of the new user requesting approval', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 0 },
{ key: 'NEW_USER_EMAIL', label: 'New User Email', description: 'Email of the new user', isRequired: true, isConditional: false, sampleValue: 'jane@example.com', sortOrder: 1 },
{ key: 'REGISTERED_AT', label: 'Registration Date', description: 'Date the user registered', isRequired: true, isConditional: false, sampleValue: 'February 16, 2026', sortOrder: 2 },
{ key: 'ADMIN_URL', label: 'Admin URL', description: 'URL to the admin users page filtered to pending approval', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/app/users?status=PENDING_APPROVAL', sortOrder: 3 },
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 4 },
],
},
{
key: 'donation-receipt',
name: 'Donation Receipt',
description: 'Receipt email sent to donors after a successful donation payment',
category: EmailTemplateCategory.PAYMENT,
subjectLine: 'Donation Receipt — {{ORGANIZATION_NAME}}',
isSystem: true,
variables: [
{ key: 'RECIPIENT_NAME', label: 'Donor Name', description: 'Name of the donor', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 0 },
{ key: 'AMOUNT', label: 'Amount', description: 'Donation amount formatted with dollar sign', isRequired: true, isConditional: false, sampleValue: '$25.00', sortOrder: 1 },
{ key: 'ORDER_ID', label: 'Order ID', description: 'Unique reference ID for the donation', isRequired: true, isConditional: false, sampleValue: 'clxyz123abc', sortOrder: 2 },
{ key: 'DONATION_DATE', label: 'Donation Date', description: 'Date the donation was completed', isRequired: true, isConditional: false, sampleValue: 'February 17, 2026', sortOrder: 3 },
{ key: 'DONOR_MESSAGE', label: 'Donor Message', description: 'Optional message from the donor', isRequired: false, isConditional: true, sampleValue: 'Keep up the great work!', sortOrder: 4 },
{ key: 'IS_ANONYMOUS', label: 'Is Anonymous', description: 'Whether the donation is anonymous', isRequired: false, isConditional: true, sampleValue: 'true', sortOrder: 5 },
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 6 },
],
},
{
key: 'product-receipt',
name: 'Product Purchase Receipt',
description: 'Receipt email sent to buyers after a successful product purchase',
category: EmailTemplateCategory.PAYMENT,
subjectLine: 'Purchase Receipt — {{ORGANIZATION_NAME}}',
isSystem: true,
variables: [
{ key: 'RECIPIENT_NAME', label: 'Buyer Name', description: 'Name of the buyer', isRequired: true, isConditional: false, sampleValue: 'John Smith', sortOrder: 0 },
{ key: 'AMOUNT', label: 'Amount', description: 'Purchase amount formatted with dollar sign', isRequired: true, isConditional: false, sampleValue: '$49.99', sortOrder: 1 },
{ key: 'ORDER_ID', label: 'Order ID', description: 'Unique order reference ID', isRequired: true, isConditional: false, sampleValue: 'clxyz456def', sortOrder: 2 },
{ key: 'PRODUCT_TITLE', label: 'Product Title', description: 'Title of the purchased product', isRequired: true, isConditional: false, sampleValue: 'Community Toolkit', sortOrder: 3 },
{ key: 'PRODUCT_TYPE', label: 'Product Type', description: 'Type of product (DIGITAL, EVENT, DONATION)', isRequired: true, isConditional: false, sampleValue: 'DIGITAL', sortOrder: 4 },
{ key: 'PURCHASE_DATE', label: 'Purchase Date', description: 'Date of purchase', isRequired: true, isConditional: false, sampleValue: 'February 17, 2026', sortOrder: 5 },
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 6 },
],
},
{
key: 'subscription-welcome',
name: 'Subscription Welcome',
description: 'Welcome email sent when a user subscribes to a plan',
category: EmailTemplateCategory.PAYMENT,
subjectLine: 'Welcome to {{PLAN_NAME}} — {{ORGANIZATION_NAME}}',
isSystem: true,
variables: [
{ key: 'RECIPIENT_NAME', label: 'User Name', description: 'Name of the subscriber', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 0 },
{ key: 'PLAN_NAME', label: 'Plan Name', description: 'Name of the subscription plan', isRequired: true, isConditional: false, sampleValue: 'Pro Plan', sortOrder: 1 },
{ key: 'AMOUNT', label: 'Amount', description: 'Subscription price formatted with dollar sign', isRequired: true, isConditional: false, sampleValue: '$9.99', sortOrder: 2 },
{ key: 'FREQUENCY', label: 'Billing Frequency', description: 'How often the subscription renews', isRequired: true, isConditional: false, sampleValue: 'per month', sortOrder: 3 },
{ key: 'RENEWAL_DATE', label: 'Renewal Date', description: 'Next renewal date', isRequired: true, isConditional: false, sampleValue: 'March 17, 2026', sortOrder: 4 },
{ key: 'SUBSCRIPTION_ID', label: 'Subscription ID', description: 'Stripe subscription ID', isRequired: true, isConditional: false, sampleValue: 'sub_1234567890', sortOrder: 5 },
{ key: 'LOGIN_URL', label: 'Login URL', description: 'URL to log in to the platform', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/login', sortOrder: 6 },
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 7 },
],
},
{
key: 'admin-shift-signup-alert',
name: 'Admin: New Shift Signup Alert',
description: 'Notification sent to admins when a volunteer signs up for a shift',
category: EmailTemplateCategory.MAP,
subjectLine: 'New shift signup — {{SHIFT_TITLE}}',
isSystem: true,
variables: [
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
{ key: 'SHIFT_TITLE', label: 'Shift Title', description: 'Title of the shift', isRequired: true, isConditional: false, sampleValue: 'Weekend Canvassing - Downtown', sortOrder: 1 },
{ key: 'SHIFT_DATE', label: 'Shift Date', description: 'Date of the shift', isRequired: true, isConditional: false, sampleValue: 'Saturday, February 22, 2026', sortOrder: 2 },
{ key: 'VOLUNTEER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer who signed up', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 3 },
{ key: 'VOLUNTEER_EMAIL', label: 'Volunteer Email', description: 'Email of the volunteer', isRequired: true, isConditional: false, sampleValue: 'jane@example.com', sortOrder: 4 },
{ key: 'SIGNUP_SOURCE', label: 'Signup Source', description: 'How the volunteer signed up (Public Form, Authenticated Volunteer)', isRequired: true, isConditional: false, sampleValue: 'Public Form', sortOrder: 5 },
{ key: 'ADMIN_URL', label: 'Admin URL', description: 'URL to the admin shifts page', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/app/map/shifts', sortOrder: 6 },
],
},
{
key: 'admin-response-submitted-alert',
name: 'Admin: Response Submitted Alert',
description: 'Notification sent to admins when a new response is submitted to the response wall',
category: EmailTemplateCategory.INFLUENCE,
subjectLine: 'New response submitted — {{CAMPAIGN_TITLE}}',
isSystem: true,
variables: [
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
{ key: 'CAMPAIGN_TITLE', label: 'Campaign Title', description: 'Title of the campaign', isRequired: true, isConditional: false, sampleValue: 'Support Climate Action Bill C-12', sortOrder: 1 },
{ key: 'REPRESENTATIVE_NAME', label: 'Representative Name', description: 'Name of the representative the response is about', isRequired: true, isConditional: false, sampleValue: 'Hon. John Smith', sortOrder: 2 },
{ key: 'RESPONSE_TYPE', label: 'Response Type', description: 'Type of response (Support, Oppose, etc.)', isRequired: true, isConditional: false, sampleValue: 'SUPPORT', sortOrder: 3 },
{ key: 'SUBMITTER_NAME', label: 'Submitter Name', description: 'Name of the person who submitted', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 4 },
{ key: 'ADMIN_URL', label: 'Admin URL', description: 'URL to the admin responses page', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/app/influence/responses', sortOrder: 5 },
],
},
{
key: 'admin-sign-requested-alert',
name: 'Admin: Sign Requested Alert',
description: 'Notification sent to admins when a resident requests a yard sign during canvassing',
category: EmailTemplateCategory.MAP,
subjectLine: 'Sign requested — {{ADDRESS}}',
isSystem: true,
variables: [
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
{ key: 'VOLUNTEER_NAME', label: 'Canvasser Name', description: 'Name of the volunteer who recorded the sign request', isRequired: true, isConditional: false, sampleValue: 'John Smith', sortOrder: 1 },
{ key: 'ADDRESS', label: 'Address', description: 'Street address where sign was requested', isRequired: true, isConditional: false, sampleValue: '123 Main Street', sortOrder: 2 },
{ key: 'SHIFT_TITLE', label: 'Shift Title', description: 'Title of the canvassing shift', isRequired: true, isConditional: false, sampleValue: 'Weekend Canvassing - Downtown', sortOrder: 3 },
{ key: 'SIGN_SIZE', label: 'Sign Size', description: 'Requested sign size', isRequired: false, isConditional: true, sampleValue: 'Large', sortOrder: 4 },
{ key: 'ADMIN_URL', label: 'Admin URL', description: 'URL to the admin canvass dashboard', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/app/canvass/dashboard', sortOrder: 5 },
],
},
{
key: 'volunteer-session-summary',
name: 'Volunteer: Canvass Session Summary',
description: 'Summary email sent to a volunteer after completing a canvassing session',
category: EmailTemplateCategory.MAP,
subjectLine: 'Canvass session summary — {{CUT_NAME}}',
isSystem: true,
variables: [
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
{ key: 'VOLUNTEER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 1 },
{ key: 'CUT_NAME', label: 'Cut/Area Name', description: 'Name of the canvassing area', isRequired: true, isConditional: false, sampleValue: 'Downtown Core', sortOrder: 2 },
{ key: 'SESSION_DATE', label: 'Session Date', description: 'Date of the session', isRequired: true, isConditional: false, sampleValue: 'Saturday, February 22, 2026', sortOrder: 3 },
{ key: 'VISIT_COUNT', label: 'Visit Count', description: 'Number of doors visited', isRequired: true, isConditional: false, sampleValue: '42', sortOrder: 4 },
{ key: 'DURATION_MINUTES', label: 'Duration (minutes)', description: 'Session duration in minutes', isRequired: true, isConditional: false, sampleValue: '95', sortOrder: 5 },
{ key: 'DISTANCE_KM', label: 'Distance (km)', description: 'Distance walked in kilometers', isRequired: true, isConditional: false, sampleValue: '2.3', sortOrder: 6 },
{ key: 'OUTCOME_BREAKDOWN', label: 'Outcome Breakdown', description: 'HTML table (email) or text list (plain text) of visit outcomes', isRequired: false, isConditional: true, sampleValue: 'Spoke With: 20, Not Home: 15, Refused: 7', sortOrder: 7 },
],
},
{
key: 'volunteer-cancellation-ack',
name: 'Volunteer: Signup Cancellation Acknowledgement',
description: 'Confirmation email sent to a volunteer when their shift signup is cancelled',
category: EmailTemplateCategory.MAP,
subjectLine: 'Signup cancelled — {{SHIFT_TITLE}}',
isSystem: true,
variables: [
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
{ key: 'VOLUNTEER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 1 },
{ key: 'SHIFT_TITLE', label: 'Shift Title', description: 'Title of the cancelled shift signup', isRequired: true, isConditional: false, sampleValue: 'Weekend Canvassing - Downtown', sortOrder: 2 },
{ key: 'SHIFT_DATE', label: 'Shift Date', description: 'Date of the shift', isRequired: true, isConditional: false, sampleValue: 'Saturday, February 22, 2026', sortOrder: 3 },
{ key: 'SHIFT_TIME', label: 'Shift Time', description: 'Time range of the shift', isRequired: true, isConditional: false, sampleValue: '10:00 AM — 2:00 PM', sortOrder: 4 },
{ key: 'SIGNUP_URL', label: 'Signup URL', description: 'URL to browse available shifts', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/shifts', sortOrder: 5 },
],
},
{
key: 'volunteer-shift-thank-you',
name: 'Volunteer: Post-Shift Thank You',
description: 'Thank-you email sent to a volunteer 2 hours after their shift ends',
category: EmailTemplateCategory.MAP,
subjectLine: 'Thank you for volunteering — {{SHIFT_TITLE}}',
isSystem: true,
variables: [
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
{ key: 'VOLUNTEER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 1 },
{ key: 'SHIFT_TITLE', label: 'Shift Title', description: 'Title of the shift', isRequired: true, isConditional: false, sampleValue: 'Weekend Canvassing - Downtown', sortOrder: 2 },
{ key: 'SHIFT_DATE', label: 'Shift Date', description: 'Date of the shift', isRequired: true, isConditional: false, sampleValue: 'Saturday, February 22, 2026', sortOrder: 3 },
{ key: 'SHIFT_TIME', label: 'Shift Time', description: 'Time range of the shift', isRequired: true, isConditional: false, sampleValue: '10:00 AM — 2:00 PM', sortOrder: 4 },
{ key: 'SHIFT_LOCATION', label: 'Shift Location', description: 'Meeting location for the shift', isRequired: false, isConditional: true, sampleValue: 'City Hall', sortOrder: 5 },
{ key: 'SIGNUP_URL', label: 'Signup URL', description: 'URL to browse upcoming shifts', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/shifts', sortOrder: 6 },
],
},
{
key: 'volunteer-reengagement',
name: 'Volunteer: Re-Engagement',
description: 'Re-engagement email sent to volunteers who have been inactive for a configurable period',
category: EmailTemplateCategory.MAP,
subjectLine: 'We miss you — {{ORGANIZATION_NAME}}',
isSystem: true,
variables: [
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
{ key: 'VOLUNTEER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 1 },
{ key: 'LAST_ACTIVITY_DATE', label: 'Last Activity Date', description: 'Date of the volunteer\'s last activity', isRequired: true, isConditional: false, sampleValue: 'January 15, 2026', sortOrder: 2 },
{ key: 'LAST_ACTIVITY_TYPE', label: 'Last Activity Type', description: 'Type of the volunteer\'s last activity', isRequired: true, isConditional: false, sampleValue: 'Shift Signup', sortOrder: 3 },
{ key: 'SIGNUP_URL', label: 'Signup URL', description: 'URL to browse upcoming shifts', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/shifts', sortOrder: 4 },
],
},
];
let seededCount = 0;
let skippedCount = 0;
for (const templateDef of templateDefinitions) {
try {
// Check if template already exists
const existing = await prisma.emailTemplate.findUnique({
where: { key: templateDef.key },
});
if (existing) {
skippedCount++;
continue;
}
// Read HTML and TXT files from filesystem
const htmlPath = path.join(templatesDir, `${templateDef.key}.html`);
const txtPath = path.join(templatesDir, `${templateDef.key}.txt`);
if (!fs.existsSync(htmlPath) || !fs.existsSync(txtPath)) {
console.warn(` ⚠️ Template files not found for ${templateDef.key}`);
continue;
}
const htmlContent = fs.readFileSync(htmlPath, 'utf-8');
const textContent = fs.readFileSync(txtPath, 'utf-8');
// Create template with variables and initial version
await prisma.$transaction(async (tx) => {
const template = await tx.emailTemplate.create({
data: {
key: templateDef.key,
name: templateDef.name,
description: templateDef.description,
category: templateDef.category,
subjectLine: templateDef.subjectLine,
htmlContent,
textContent,
isSystem: templateDef.isSystem,
isActive: true,
createdByUserId: admin.id,
variables: {
create: templateDef.variables,
},
},
});
// Create initial version
await tx.emailTemplateVersion.create({
data: {
templateId: template.id,
versionNumber: 1,
subjectLine: templateDef.subjectLine,
htmlContent,
textContent,
changeNotes: 'Initial migration from filesystem',
createdByUserId: admin.id,
},
});
});
seededCount++;
} catch (error) {
console.error(` ❌ Error seeding template ${templateDef.key}:`, error);
}
}
console.log(`Email templates seeded: ${seededCount} created, ${skippedCount} skipped`);
}
/**
* Seed default SMS notification templates
*/
async function seedSmsNotificationTemplates() {
console.log('Seeding SMS notification templates...');
const templates = [
{
name: 'shift-reminder',
template: 'Hi {name}, reminder: {shiftTitle} is tomorrow at {shiftTime}. Location: {shiftLocation}',
description: 'Sent before a volunteer shift as a reminder',
category: 'notification',
},
{
name: 'shift-signup-confirm',
template: "Hi {name}, you're signed up for {shiftTitle} on {shiftDate} at {shiftTime}. See you there!",
description: 'Sent when a volunteer signs up for a shift',
category: 'notification',
},
{
name: 'volunteer-welcome',
template: 'Welcome to the team, {name}! Thanks for signing up as a volunteer.',
description: 'Sent when a new volunteer account is created',
category: 'notification',
},
];
let seeded = 0;
let skipped = 0;
for (const t of templates) {
const existing = await prisma.smsMessageTemplate.findFirst({ where: { name: t.name } });
if (existing) {
skipped++;
continue;
}
await prisma.smsMessageTemplate.create({
data: {
name: t.name,
template: t.template,
description: t.description,
category: t.category,
},
});
seeded++;
}
console.log(`SMS notification templates seeded: ${seeded} created, ${skipped} skipped`);
}
/**
* Seed pre-made gallery ads
*/
async function seedGalleryAds() {
console.log('Seeding gallery ads...');
const defaultAds = [
{
type: 'system',
variant: 'standard',
title: 'Join the Community',
subtitle: 'Create a free account to upvote, comment, and save favorites',
ctaText: 'Sign Up Free',
ctaStyle: 'primary',
linkUrl: '/login',
visibility: 'anonymous',
frequency: 8,
position: 1,
iconEmoji: null,
bgColor: null,
imagePath: null,
placements: ['gallery', 'docs'],
},
{
type: 'payment_subscribe',
variant: 'highlight',
title: 'Unlock Premium Content',
subtitle: 'Subscribe for exclusive videos, early access, and more',
ctaText: 'View Plans',
ctaStyle: 'primary',
linkUrl: '/pricing',
visibility: 'non_subscriber',
frequency: 12,
position: 2,
iconEmoji: null,
bgColor: null,
imagePath: null,
placements: ['gallery', 'pricing', 'landing_page', 'docs'],
},
{
type: 'payment_donate',
variant: 'standard',
title: 'Support Our Mission',
subtitle: 'Your donation helps us create more content',
ctaText: 'Donate Now',
ctaStyle: 'primary',
linkUrl: '/donate',
visibility: 'everyone',
frequency: 18,
position: 3,
iconEmoji: null,
bgColor: null,
imagePath: null,
placements: ['gallery', 'donate', 'landing_page', 'docs'],
},
{
type: 'payment_shop',
variant: 'standard',
title: 'Browse the Shop',
subtitle: 'Exclusive merchandise, downloads, and event tickets',
ctaText: 'Shop Now',
ctaStyle: 'primary',
linkUrl: '/shop',
visibility: 'everyone',
frequency: 24,
position: 4,
iconEmoji: null,
bgColor: null,
imagePath: null,
placements: ['gallery', 'shop', 'landing_page'],
},
{
type: 'system',
variant: 'standard',
title: 'Take Action',
subtitle: 'Join an advocacy campaign and make your voice heard',
ctaText: 'View Campaigns',
ctaStyle: 'primary',
linkUrl: '/campaigns',
visibility: 'everyone',
frequency: 18,
position: 5,
iconEmoji: null,
bgColor: null,
imagePath: null,
placements: ['gallery', 'campaigns_list', 'shifts', 'landing_page', 'docs'],
},
{
type: 'system',
variant: 'standard',
title: 'Volunteer With Us',
subtitle: 'Sign up for a shift and help make a difference',
ctaText: 'See Shifts',
ctaStyle: 'primary',
linkUrl: '/shifts',
visibility: 'everyone',
frequency: 24,
position: 6,
iconEmoji: null,
bgColor: null,
imagePath: null,
placements: ['gallery', 'shifts', 'campaigns_list', 'landing_page'],
},
];
let seeded = 0;
let skipped = 0;
for (const ad of defaultAds) {
const existing = await prisma.ad.findFirst({
where: { type: ad.type, title: ad.title },
});
if (existing) {
skipped++;
continue;
}
await prisma.ad.create({
data: {
...ad,
isSystemAd: true,
isActive: false,
},
});
seeded++;
}
console.log(`Gallery ads seeded: ${seeded} created, ${skipped} skipped`);
}
main()
.catch((e) => {
console.error('Seed error:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});