// 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 emailServices = require("../services/email"); 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 emailServices.alphaInvitation.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 emailServices.alphaInvitation.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 { // Verify database connection await sequelize.authenticate(); if (!command || command === "help") { console.log(` Alpha Invitation Management CLI Usage: node scripts/manageAlphaInvitations.js [options] Commands: add [notes] Add a new alpha invitation list [filter] List all invitations (filter: all|pending|active|revoked|unused) revoke Revoke an invitation code restore Restore a revoked invitation resend Resend an invitation email bulk 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 [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 \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 \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 \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 \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, };