This commit is contained in:
jackiettran
2025-10-30 15:38:57 -04:00
parent d1cb857aa7
commit ee3a6fd8e1
13 changed files with 1400 additions and 12 deletions

View File

@@ -0,0 +1,480 @@
// Load environment config
const env = process.env.NODE_ENV || "dev";
const envFile = `.env.${env}`;
require("dotenv").config({ path: envFile });
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const { AlphaInvitation, User, sequelize } = require("../models");
const emailService = require("../services/emailService");
const logger = require("../utils/logger");
// Generate unique alpha code
async function generateUniqueAlphaCode() {
let code;
let exists = true;
while (exists) {
// Generate exactly 8 random alphanumeric characters
const chars = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789";
let randomStr = "";
for (let i = 0; i < 8; i++) {
const randomIndex = crypto.randomInt(0, chars.length);
randomStr += chars[randomIndex];
}
code = `ALPHA-${randomStr}`;
// Check if code exists
const existing = await AlphaInvitation.findOne({ where: { code } });
exists = !!existing;
}
return code;
}
// Normalize email
function normalizeEmail(email) {
return email.toLowerCase().trim();
}
// Add invitation
async function addInvitation(email, notes = "") {
try {
email = normalizeEmail(email);
// Check if invitation already exists for this email
const existing = await AlphaInvitation.findOne({ where: { email } });
if (existing) {
console.log(`\n❌ Invitation already exists for ${email}`);
console.log(` Code: ${existing.code}`);
console.log(` Status: ${existing.status}`);
console.log(` Created: ${existing.createdAt}`);
return null;
}
// Generate unique code
const code = await generateUniqueAlphaCode();
// Create invitation
const invitation = await AlphaInvitation.create({
code,
email,
status: "pending",
notes,
});
// Send invitation email
let emailSent = false;
try {
await emailService.sendAlphaInvitation(email, code);
emailSent = true;
} catch (emailError) {
console.log(`\n⚠️ Warning: Failed to send email to ${email}`);
console.log(` Error: ${emailError.message}`);
console.log(` Invitation created but email not sent.`);
}
console.log(`\n✅ Alpha invitation created successfully!`);
console.log(` Email: ${email}`);
console.log(` Code: ${code}`);
console.log(` Email sent: ${emailSent ? "Yes" : "No"}`);
if (notes) {
console.log(` Notes: ${notes}`);
}
return invitation;
} catch (error) {
console.error(`\n❌ Error creating invitation: ${error.message}`);
throw error;
}
}
// Resend invitation
async function resendInvitation(emailOrCode) {
try {
const input = emailOrCode.trim();
let invitation;
// Try to find by code first (if it looks like a code), otherwise by email
if (input.toUpperCase().startsWith("ALPHA-")) {
invitation = await AlphaInvitation.findOne({
where: { code: input.toUpperCase() }
});
} else {
invitation = await AlphaInvitation.findOne({
where: { email: normalizeEmail(input) }
});
}
if (!invitation) {
console.log(`\n❌ Invitation not found: ${input}`);
return null;
}
// Check if revoked
if (invitation.status === "revoked") {
console.log(`\n❌ Cannot resend revoked invitation`);
console.log(` Code: ${invitation.code}`);
console.log(` Email: ${invitation.email}`);
return null;
}
// Warn if already used
if (invitation.usedBy) {
console.log(`\n⚠️ Warning: This invitation has already been used`);
console.log(` Used by user ID: ${invitation.usedBy}`);
console.log(` Continuing with resend...\n`);
}
// Resend the email
try {
await emailService.sendAlphaInvitation(invitation.email, invitation.code);
console.log(`\n✅ Alpha invitation resent successfully!`);
console.log(` Email: ${invitation.email}`);
console.log(` Code: ${invitation.code}`);
console.log(` Status: ${invitation.status}`);
return invitation;
} catch (emailError) {
console.error(`\n❌ Error sending email: ${emailError.message}`);
throw emailError;
}
} catch (error) {
console.error(`\n❌ Error resending invitation: ${error.message}`);
throw error;
}
}
// List invitations
async function listInvitations(filter = "all") {
try {
let where = {};
if (filter === "pending") {
where.status = "pending";
where.usedBy = null;
} else if (filter === "active") {
where.status = "active";
} else if (filter === "revoked") {
where.status = "revoked";
} else if (filter === "unused") {
where.usedBy = null;
}
const invitations = await AlphaInvitation.findAll({
where,
include: [
{
model: User,
as: "user",
attributes: ["id", "email", "firstName", "lastName"],
},
],
order: [["createdAt", "DESC"]],
});
console.log(
`\n📋 Alpha Invitations (${invitations.length} total, filter: ${filter})\n`
);
console.log("─".repeat(100));
console.log(
"CODE".padEnd(15) +
"EMAIL".padEnd(30) +
"STATUS".padEnd(10) +
"USED BY".padEnd(25) +
"CREATED"
);
console.log("─".repeat(100));
if (invitations.length === 0) {
console.log("No invitations found.");
} else {
invitations.forEach((inv) => {
const usedBy = inv.user
? `${inv.user.firstName} ${inv.user.lastName}`
: "-";
const created = new Date(inv.createdAt).toLocaleDateString();
console.log(
inv.code.padEnd(15) +
inv.email.padEnd(30) +
inv.status.padEnd(10) +
usedBy.padEnd(25) +
created
);
});
}
console.log("─".repeat(100));
// Summary
const stats = {
total: invitations.length,
pending: invitations.filter((i) => i.status === "pending" && !i.usedBy)
.length,
active: invitations.filter((i) => i.status === "active" && i.usedBy)
.length,
revoked: invitations.filter((i) => i.status === "revoked").length,
};
console.log(
`\nSummary: ${stats.pending} pending | ${stats.active} active | ${stats.revoked} revoked\n`
);
return invitations;
} catch (error) {
console.error(`\n❌ Error listing invitations: ${error.message}`);
throw error;
}
}
// Revoke invitation
async function revokeInvitation(code) {
try {
code = code.trim().toUpperCase();
const invitation = await AlphaInvitation.findOne({ where: { code } });
if (!invitation) {
console.log(`\n❌ Invitation not found with code: ${code}`);
return null;
}
if (invitation.status === "revoked") {
console.log(`\n⚠️ Invitation is already revoked: ${code}`);
return invitation;
}
await invitation.update({ status: "revoked" });
console.log(`\n✅ Invitation revoked successfully!`);
console.log(` Code: ${code}`);
console.log(` Email: ${invitation.email}`);
return invitation;
} catch (error) {
console.error(`\n❌ Error revoking invitation: ${error.message}`);
throw error;
}
}
// Restore invitation
async function restoreInvitation(code) {
try {
code = code.trim().toUpperCase();
const invitation = await AlphaInvitation.findOne({ where: { code } });
if (!invitation) {
console.log(`\n❌ Invitation not found with code: ${code}`);
return null;
}
if (invitation.status !== "revoked") {
console.log(`\n⚠️ Invitation is not revoked (current status: ${invitation.status})`);
console.log(` Code: ${code}`);
console.log(` Email: ${invitation.email}`);
return invitation;
}
// Determine the appropriate status to restore to
const newStatus = invitation.usedBy ? "active" : "pending";
await invitation.update({ status: newStatus });
console.log(`\n✅ Invitation restored successfully!`);
console.log(` Code: ${code}`);
console.log(` Email: ${invitation.email}`);
console.log(` Status: ${newStatus} (${invitation.usedBy ? 'was previously used' : 'never used'})`);
return invitation;
} catch (error) {
console.error(`\n❌ Error restoring invitation: ${error.message}`);
throw error;
}
}
// Bulk import from CSV
async function bulkImport(csvPath) {
try {
if (!fs.existsSync(csvPath)) {
console.log(`\n❌ File not found: ${csvPath}`);
return;
}
const csvContent = fs.readFileSync(csvPath, "utf-8");
const lines = csvContent.split("\n").filter((line) => line.trim());
// Skip header if present
const hasHeader = lines[0].toLowerCase().includes("email");
const dataLines = hasHeader ? lines.slice(1) : lines;
console.log(
`\n📥 Importing ${dataLines.length} invitations from ${csvPath}...\n`
);
let successCount = 0;
let failCount = 0;
for (const line of dataLines) {
const [email, notes] = line.split(",").map((s) => s.trim());
if (!email) {
console.log(`⚠️ Skipping empty line`);
failCount++;
continue;
}
try {
await addInvitation(email, notes || "");
successCount++;
} catch (error) {
console.log(`❌ Failed to add ${email}: ${error.message}`);
failCount++;
}
}
console.log(`\n✅ Bulk import completed!`);
console.log(` Success: ${successCount}`);
console.log(` Failed: ${failCount}`);
console.log(` Total: ${dataLines.length}\n`);
} catch (error) {
console.error(`\n❌ Error during bulk import: ${error.message}`);
throw error;
}
}
// Main CLI handler
async function main() {
const args = process.argv.slice(2);
const command = args[0];
try {
// Sync database
await sequelize.sync();
if (!command || command === "help") {
console.log(`
Alpha Invitation Management CLI
Usage:
node scripts/manageAlphaInvitations.js <command> [options]
Commands:
add <email> [notes] Add a new alpha invitation
list [filter] List all invitations (filter: all|pending|active|revoked|unused)
revoke <code> Revoke an invitation code
restore <code> Restore a revoked invitation
resend <email|code> Resend an invitation email
bulk <csvPath> Bulk import invitations from CSV file
Examples:
node scripts/manageAlphaInvitations.js add alice@example.com "Product team"
node scripts/manageAlphaInvitations.js list pending
node scripts/manageAlphaInvitations.js revoke ALPHA-ABC12345
node scripts/manageAlphaInvitations.js restore ALPHA-ABC12345
node scripts/manageAlphaInvitations.js resend alice@example.com
node scripts/manageAlphaInvitations.js bulk invitations.csv
CSV Format:
email,notes
alice@example.com,Product team
bob@example.com,Early adopter
`);
} else if (command === "add") {
const email = args[1];
const notes = args.slice(2).join(" ");
if (!email) {
console.log("\n❌ Error: Email is required");
console.log(
"Usage: node scripts/manageAlphaInvitations.js add <email> [notes]\n"
);
process.exit(1);
}
await addInvitation(email, notes);
} else if (command === "list") {
const filter = args[1] || "all";
await listInvitations(filter);
} else if (command === "revoke") {
const code = args[1];
if (!code) {
console.log("\n❌ Error: Code is required");
console.log(
"Usage: node scripts/manageAlphaInvitations.js revoke <code>\n"
);
process.exit(1);
}
await revokeInvitation(code);
} else if (command === "resend") {
const emailOrCode = args[1];
if (!emailOrCode) {
console.log("\n❌ Error: Email or code is required");
console.log(
"Usage: node scripts/manageAlphaInvitations.js resend <email|code>\n"
);
process.exit(1);
}
await resendInvitation(emailOrCode);
} else if (command === "restore") {
const code = args[1];
if (!code) {
console.log("\n❌ Error: Code is required");
console.log(
"Usage: node scripts/manageAlphaInvitations.js restore <code>\n"
);
process.exit(1);
}
await restoreInvitation(code);
} else if (command === "bulk") {
const csvPath = args[1];
if (!csvPath) {
console.log("\n❌ Error: CSV path is required");
console.log(
"Usage: node scripts/manageAlphaInvitations.js bulk <csvPath>\n"
);
process.exit(1);
}
await bulkImport(path.resolve(csvPath));
} else {
console.log(`\n❌ Unknown command: ${command}`);
console.log(
"Run 'node scripts/manageAlphaInvitations.js help' for usage information\n"
);
process.exit(1);
}
process.exit(0);
} catch (error) {
console.error(`\n❌ Fatal error: ${error.message}`);
console.error(error.stack);
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
main();
}
module.exports = {
addInvitation,
listInvitations,
revokeInvitation,
restoreInvitation,
resendInvitation,
bulkImport,
generateUniqueAlphaCode,
};