alpha
This commit is contained in:
480
backend/scripts/manageAlphaInvitations.js
Normal file
480
backend/scripts/manageAlphaInvitations.js
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user