481 lines
13 KiB
JavaScript
481 lines
13 KiB
JavaScript
// 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,
|
|
};
|