new-site/scripts/rescue-mark.mjs
justin e87715aee7 fix(portal): onboarding/login links last 7 days, not 60 min
The rescue onboarding emails hardcoded a 60-minute expiry -- way too short for a
paid customer who hasn't engaged yet (they may not check email for hours/days),
so Paul's and Mitchell's links expired before they used them. Onboarding links
now last 7 days (ONBOARDING_TTL_MINUTES); the standard security password-RESET
window bumped 30min -> 2h. Re-issued fresh 7-day links to all 3 affected
customers (none had set a password yet) via reissue-onboarding-links.mjs, cc'd.
2026-06-09 22:50:09 -05:00

77 lines
5 KiB
JavaScript

/**
* Rescue Mark Adams (mark@adamslumber.com) - paid Jun 1 for an MCS-150 update via
* card, but the card/webhook path never created his portal `customers` row (the
* login bug), so he could not log in or complete intake -> his MCS-150 is stuck
* "NEEDS MANUAL FILING". Now that the login bug is fixed:
* 1. create his customers row,
* 2. send a password-set link + the intake link (CC justin), so he can log in
* and complete intake to unblock the filing.
*
* Run: docker exec performancewest-api-1 node /app/scripts/rescue-mark.mjs
*/
import pg from "pg";
import crypto from "crypto";
import nodemailer from "nodemailer";
const EMAIL = "mark@adamslumber.com";
const CC = "justin@performancewest.net";
const SITE = process.env.DOMAIN ? `https://${process.env.DOMAIN}` : "https://performancewest.net";
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const mailer = nodemailer.createTransport({
host: process.env.SMTP_HOST || "co.carrierone.com",
port: parseInt(process.env.SMTP_PORT || "587", 10),
secure: false,
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
});
const FROM = process.env.SMTP_FROM || "Performance West <noreply@performancewest.net>";
const { rows: orders } = await pool.query(
`SELECT order_number, service_name, service_slug, customer_name, intake_data
FROM compliance_orders WHERE lower(customer_email)=lower($1) ORDER BY created_at`,
[EMAIL],
);
if (!orders.length) { console.log("no orders for", EMAIL); process.exit(1); }
const name = orders[0].customer_name || "there";
const firstName = name.split(" ")[0];
let company = null;
try { const i = typeof orders[0].intake_data === "string" ? JSON.parse(orders[0].intake_data) : orders[0].intake_data; company = i?.company || i?.legal_name || i?.entity_name || null; } catch {}
// 1) customers row
const c = await pool.query(
`INSERT INTO customers (email, name, company) VALUES ($1,$2,$3)
ON CONFLICT (email) DO UPDATE SET name=COALESCE(customers.name,EXCLUDED.name), company=COALESCE(customers.company,EXCLUDED.company)
RETURNING id, (password_hash IS NOT NULL) AS has_pw`, [EMAIL, name, company]);
console.log(`[rescue] customers row id=${c.rows[0].id} email=${EMAIL} has_password=${c.rows[0].has_pw}`);
// 2) password-set link
const token = crypto.randomBytes(32).toString("hex");
await pool.query(`INSERT INTO password_reset_tokens (customer_id, token, expires_at) VALUES ($1,$2,$3)`,
[c.rows[0].id, token, new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)]);
const resetLink = `${SITE}/account/reset-password?token=${token}`;
const items = orders.map(o => {
const url = `${SITE}/order/${o.service_slug}?order=${o.order_number}`;
const note = o.service_slug === "mcs150-update"
? " (we will prepare the update from your intake, then send it to you to sign before we submit to FMCSA)"
: "";
return `<li style="margin:8px 0;font-size:14px;color:#374151"><a href="${url}" style="color:#1e40af;font-weight:600;text-decoration:underline">${o.service_name}</a> <span style="color:#888;font-family:monospace">(${o.order_number})</span>${note}</li>`;
}).join("");
await mailer.sendMail({
from: FROM, to: EMAIL, cc: CC,
subject: "Your Performance West order - log in and complete your intake",
html: `<div style="font-family:Arial,sans-serif;max-width:580px;margin:0 auto;padding:24px;color:#222">
<h2 style="color:#1a2744;margin:0 0 8px">You're all set, ${firstName}</h2>
<p>Thanks for your order. We had a delivery issue that kept our earlier emails from reaching you - that's fixed now, so here is everything you need to get your filing moving.</p>
<p style="margin:18px 0 6px"><strong>1. Set your password to log in</strong> (valid for 7 days):</p>
<p style="margin:6px 0 18px"><a href="${resetLink}" style="background:#2d4e78;color:#fff;padding:12px 28px;border-radius:8px;text-decoration:none;font-weight:600">Set my password &rarr;</a></p>
<p style="font-size:13px;color:#666;margin:0 0 18px">Or paste this link: ${resetLink}</p>
<p style="margin:18px 0 6px"><strong>2. Complete the short intake form</strong> so we have what we need to prepare your filing (about 2-5 minutes):</p>
<ul style="padding-left:18px">${items}</ul>
<p style="font-size:13px;color:#666">Once your intake is in, we prepare the MCS-150 update and send it to you to review and sign - we never submit to FMCSA without your signature. Questions? Reply here or call 1-888-411-0383.</p>
<p style="font-size:12px;color:#9ca3af">Performance West Inc. &middot; performancewest.net &middot; 1-888-411-0383</p>
</div>`,
text: `Hi ${firstName}, set your password to log in: ${resetLink} (valid 7 days). Then complete your intake: ${orders.map(o => o.service_name + " (" + o.order_number + "): " + SITE + "/order/" + o.service_slug + "?order=" + o.order_number).join("; ")}. The MCS-150 will be sent to you to sign before we submit to FMCSA. Questions? 1-888-411-0383.`,
});
console.log(`[rescue] login + intake email sent to ${EMAIL} (cc ${CC}) for ${orders.length} order(s)`);
await pool.end();