mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -1,114 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('email_templates', {
|
||||
email_template_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
type: { type: Sequelize.STRING(100), allowNull: false, unique: true },
|
||||
label: { type: Sequelize.STRING(150), allowNull: false },
|
||||
subject: { type: Sequelize.STRING(255), allowNull: false },
|
||||
html_body: { type: Sequelize.TEXT, allowNull: false },
|
||||
is_system: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false },
|
||||
});
|
||||
|
||||
const FONT = 'font-family: Arial, sans-serif; font-size: 14px; color: #000;';
|
||||
const now = new Date();
|
||||
|
||||
// Seed the 8 built-in types that services/email.service.js's sendEmail()
|
||||
// looks up by name — these are marked is_system so they can't be
|
||||
// deleted/renamed from the admin CRUD. Body content only (no <html>/<body>
|
||||
// wrapper) — the header/footer layout stays fixed in code, not admin-editable.
|
||||
await queryInterface.bulkInsert('email_templates', [
|
||||
{
|
||||
type: 'OTP', label: 'OTP Verification', is_system: true,
|
||||
subject: 'Email OTP Verification - STARR System',
|
||||
html_body: `<p>Dear User,</p>
|
||||
<p>Please use the One-Time Password (OTP) below to verify your email address. This code is valid for <strong>{{expiryMinutes}} minutes</strong>.</p>
|
||||
<p style="font-size: 28px; font-weight: bold; letter-spacing: 6px;">{{otp}}</p>
|
||||
<p>For security reasons, please do not share this code with anyone. If you did not request this, please contact the administrator.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'WELCOME', label: 'Welcome Email', is_system: true,
|
||||
subject: 'Welcome to STARR System',
|
||||
html_body: `<p>Dear {{name}},</p>
|
||||
<p>We are pleased to welcome you to the STARR System. Your account has been successfully created and is now ready for use.</p>
|
||||
<p>You may now access your dashboard and begin using the available services.</p>
|
||||
<p>We look forward to supporting you.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'PASSWORD_CHANGED', label: 'Password Changed', is_system: true,
|
||||
subject: 'Password Update Confirmation - STARR System',
|
||||
html_body: `<p>Dear User,</p>
|
||||
<p>This is to confirm that your account password has been successfully changed.</p>
|
||||
<p>If you did not perform this action, please reset your password immediately or contact support.</p>
|
||||
<p>For your security, we recommend using a strong and unique password.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'ADDED_TO_GROUP', label: 'Added to Group', is_system: true,
|
||||
subject: 'Group Assignment Notification - STARR System',
|
||||
html_body: `<p>Dear User,</p>
|
||||
<p>You have been assigned to the group <strong>{{groupName}}</strong> in the STARR System.</p>
|
||||
<p>This assignment grants you access to shared resources and collaboration tools within the group.</p>
|
||||
<p>Please log in to your account to view group details.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'TASK_ASSIGNED', label: 'Task Assigned', is_system: true,
|
||||
subject: 'New Task Assignment - STARR System',
|
||||
html_body: `<p>Dear User,</p>
|
||||
<p>You have been assigned a new task in the STARR System.</p>
|
||||
<table style="${FONT}">
|
||||
<tr><td><strong>Task</strong></td><td>{{taskTitle}}</td></tr>
|
||||
<tr><td><strong>Due Date</strong></td><td>{{dueDate}}</td></tr>
|
||||
</table>
|
||||
<p>Kindly ensure completion within the specified timeframe.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'BAN_LIFTED', label: 'Ban Lifted', is_system: true,
|
||||
subject: 'Account Suspension Lifted - STARR System',
|
||||
html_body: `<p>Dear {{name}},</p>
|
||||
<p>We are writing to inform you that the suspension on your account (<strong>{{email}}</strong>) has been lifted effective <strong>{{date}}</strong>.</p>
|
||||
<p>You may now log in and resume access to all services within the STARR System.</p>
|
||||
<p>If you have any concerns, please do not hesitate to contact your administrator.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'BANNED', label: 'Account Banned', is_system: true,
|
||||
subject: 'Account Suspension Notice - STARR System',
|
||||
html_body: `<p>Dear {{name}},</p>
|
||||
<p>Your account (<strong>{{email}}</strong>) has been {{duration_word}} suspended from the STARR System effective <strong>{{date}}</strong>.</p>
|
||||
<table style="${FONT}">
|
||||
<tr><td><strong>Reason</strong></td><td>{{reason}}</td></tr>
|
||||
<tr><td><strong>Duration</strong></td><td>{{duration_label}}</td></tr>
|
||||
</table>
|
||||
<p>During this period, access to all system services has been revoked.{{suspension_note}}</p>
|
||||
<p>If you believe this was made in error, please contact your administrator.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
{
|
||||
type: 'ADD_STAFF', label: 'Staff Account Created', is_system: true,
|
||||
subject: 'Your Staff Account Has Been Created - STARR System',
|
||||
html_body: `<p>Dear {{name}},</p>
|
||||
<p>Your staff account has been successfully created in the STARR System. Below are your login credentials:</p>
|
||||
<table style="${FONT}">
|
||||
<tr><td><strong>Email</strong></td><td>{{email}}</td></tr>
|
||||
<tr><td><strong>Password</strong></td><td>{{password}}</td></tr>
|
||||
</table>
|
||||
<p>This temporary password is valid for <strong>{{expiryHours}} hours</strong>. You will be required to change it upon first login.</p>
|
||||
<p>If you did not request this account or believe this was created in error, please contact your administrator immediately to have it deactivated.</p>
|
||||
<p>For security, please do not share your credentials with anyone. If you need a new password, contact your administrator.</p>`,
|
||||
createdAt: now, updatedAt: now,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.dropTable('email_templates');
|
||||
},
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('email_templates', 'category', {
|
||||
type: Sequelize.ENUM('announcement', 'advertisement', 'system', 'other'),
|
||||
allowNull: false,
|
||||
defaultValue: 'other',
|
||||
});
|
||||
|
||||
// The 8 built-in (is_system) templates are all account/auth notifications.
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE email_templates SET category = 'system' WHERE is_system = true;
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('email_templates', 'category');
|
||||
await queryInterface.sequelize.query(`DROP TYPE IF EXISTS "enum_email_templates_category";`);
|
||||
},
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Live `subject`/`html_body` become nullable — a brand-new template can
|
||||
// now exist as a pure draft with no live content at all until it's sent.
|
||||
await queryInterface.changeColumn('email_templates', 'subject', {
|
||||
type: Sequelize.STRING(255), allowNull: true,
|
||||
});
|
||||
await queryInterface.changeColumn('email_templates', 'html_body', {
|
||||
type: Sequelize.TEXT, allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('email_templates', 'status', {
|
||||
type: Sequelize.ENUM('draft', 'sent'), allowNull: false, defaultValue: 'draft',
|
||||
});
|
||||
|
||||
// Edits to a 'sent' template land here first — sendEmail() only ever reads
|
||||
// the live subject/html_body columns, never these — until an admin
|
||||
// explicitly re-sends (publishes), the pending edit can't affect real mail.
|
||||
await queryInterface.addColumn('email_templates', 'draft_subject', {
|
||||
type: Sequelize.STRING(255), allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('email_templates', 'draft_html_body', {
|
||||
type: Sequelize.TEXT, allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('email_templates', 'last_sent_at', {
|
||||
type: Sequelize.DATE, allowNull: true,
|
||||
});
|
||||
|
||||
// Every row created before this migration already had required subject/
|
||||
// html_body — meaning it was already "operating" in the old single-state
|
||||
// world. Backfill them all as sent.
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE email_templates SET status = 'sent', last_sent_at = "updatedAt";
|
||||
`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('email_templates', 'last_sent_at');
|
||||
await queryInterface.removeColumn('email_templates', 'draft_html_body');
|
||||
await queryInterface.removeColumn('email_templates', 'draft_subject');
|
||||
await queryInterface.removeColumn('email_templates', 'status');
|
||||
await queryInterface.sequelize.query(`DROP TYPE IF EXISTS "enum_email_templates_status";`);
|
||||
},
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('email_broadcasts', {
|
||||
email_broadcast_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
email_template_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'email_templates', key: 'email_template_id' } },
|
||||
target_type: { type: Sequelize.ENUM('admin', 'user', 'both', 'task_list', 'course', 'tier_plan'), allowNull: false },
|
||||
target_id: { type: Sequelize.STRING(64), allowNull: true },
|
||||
status: { type: Sequelize.ENUM('queued', 'sending', 'completed', 'canceled'), allowNull: false, defaultValue: 'queued' },
|
||||
total_recipients: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
|
||||
sent_count: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
|
||||
failed_count: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
|
||||
started_at: { type: Sequelize.DATE, allowNull: true },
|
||||
completed_at: { type: Sequelize.DATE, allowNull: true },
|
||||
createdBy: { type: Sequelize.BIGINT, allowNull: true },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false },
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('email_broadcasts', ['status']);
|
||||
|
||||
await queryInterface.createTable('email_broadcast_recipients', {
|
||||
email_broadcast_recipient_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
email_broadcast_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'email_broadcasts', key: 'email_broadcast_id' } },
|
||||
user_id: { type: Sequelize.BIGINT, allowNull: true },
|
||||
email: { type: Sequelize.STRING(255), allowNull: false },
|
||||
name: { type: Sequelize.STRING(255), allowNull: true },
|
||||
status: { type: Sequelize.ENUM('pending', 'sent', 'failed'), allowNull: false, defaultValue: 'pending' },
|
||||
error: { type: Sequelize.TEXT, allowNull: true },
|
||||
sent_at: { type: Sequelize.DATE, allowNull: true },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false },
|
||||
});
|
||||
|
||||
// The cron's core query: "give me the next pending batch for a broadcast".
|
||||
await queryInterface.addIndex('email_broadcast_recipients', ['email_broadcast_id', 'status']);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.dropTable('email_broadcast_recipients');
|
||||
await queryInterface.dropTable('email_broadcasts');
|
||||
await queryInterface.sequelize.query(`DROP TYPE IF EXISTS "enum_email_broadcast_recipients_status";`);
|
||||
await queryInterface.sequelize.query(`DROP TYPE IF EXISTS "enum_email_broadcasts_status";`);
|
||||
await queryInterface.sequelize.query(`DROP TYPE IF EXISTS "enum_email_broadcasts_target_type";`);
|
||||
},
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// The admin editor now authors in Markdown (converted client-side to the
|
||||
// HTML that subject/html_body already require) — these columns retain the
|
||||
// original Markdown purely so reopening a template for editing shows the
|
||||
// human-readable source again instead of the compiled HTML. Templates
|
||||
// created before this migration (all 8 system templates included) have
|
||||
// no Markdown source — html_body/draft_html_body remain hand-written HTML
|
||||
// for them, and the editor falls back to editing that directly.
|
||||
await queryInterface.addColumn('email_templates', 'body_markdown', {
|
||||
type: Sequelize.TEXT, allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('email_templates', 'draft_body_markdown', {
|
||||
type: Sequelize.TEXT, allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('email_templates', 'draft_body_markdown');
|
||||
await queryInterface.removeColumn('email_templates', 'body_markdown');
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user