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.
This commit is contained in:
justin 2026-06-09 22:50:09 -05:00
parent a6d2f10149
commit e87715aee7
5 changed files with 75 additions and 10 deletions

View file

@ -0,0 +1,59 @@
/**
* Re-issue LONG-LIVED (7-day) password-set links to the 3 rescued customers
* whose earlier onboarding links were wrongly set to 60 minutes (and have since
* expired). None of them set a password yet, so this gets them a working link.
* CC justin. Onboarding links should be days, not minutes -- these are paid
* customers who just need to get in, not a security reset.
*
* Run: docker exec performancewest-api-1 node /app/scripts/reissue-onboarding-links.mjs
*/
import pg from "pg";
import crypto from "crypto";
import nodemailer from "nodemailer";
const CC = "justin@performancewest.net";
const SITE = process.env.DOMAIN ? `https://${process.env.DOMAIN}` : "https://performancewest.net";
const TTL_DAYS = 7;
const EMAILS = ["mark@adamslumber.com", "synthetic@pipeline.com", "mitchell@allenscrapmetal.com"];
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>";
for (const email of EMAILS) {
const cust = await pool.query(`SELECT id, name, (password_hash IS NOT NULL) AS has_pw FROM customers WHERE lower(email)=lower($1)`, [email]);
if (!cust.rows.length) { console.log(`[reissue] no customers row for ${email} - skip`); continue; }
const c = cust.rows[0];
if (c.has_pw) { console.log(`[reissue] ${email} already set a password - skip`); continue; }
const firstName = (c.name || "there").split(" ")[0];
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.id, token, new Date(Date.now() + TTL_DAYS * 24 * 60 * 60 * 1000)],
);
const link = `${SITE}/account/reset-password?token=${token}`;
await mailer.sendMail({
from: FROM, to: email, cc: CC,
subject: "Your Performance West login link (valid 7 days)",
html: `<div style="font-family:Arial,sans-serif;max-width:540px;margin:0 auto;padding:24px;color:#222">
<h2 style="color:#1a2744;margin:0 0 8px">Set your password and log in, ${firstName}</h2>
<p>Apologies - the link we sent earlier expired too quickly. Here is a fresh one that stays valid for <strong>7 days</strong>.</p>
<p style="margin:20px 0"><a href="${link}" 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">Or paste this link into your browser:<br>${link}</p>
<p style="font-size:13px;color:#666">Once you're in, you can track your filings and complete any remaining intake. 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}, apologies the earlier link expired too fast. Set your password to log in (valid 7 days): ${link}. Questions? 1-888-411-0383.`,
});
console.log(`[reissue] 7-day login link sent to ${email} (cc ${CC})`);
}
await pool.end();
console.log("[reissue] DONE");

View file

@ -46,7 +46,7 @@ console.log(`[rescue] customers row id=${c.rows[0].id} email=${EMAIL} has_passwo
// 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() + 60 * 60 * 1000)]);
[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 => {
@ -63,7 +63,7 @@ await mailer.sendMail({
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> (expires in 60 minutes):</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>
@ -71,7 +71,7 @@ await mailer.sendMail({
<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} (expires 60 min). 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.`,
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();

View file

@ -30,7 +30,7 @@ const { rows: orders } = await pool.query(
const token = crypto.randomBytes(32).toString("hex");
await pool.query(
`INSERT INTO password_reset_tokens (customer_id, token, expires_at) VALUES ($1,$2,$3)`,
[customer.id, token, new Date(Date.now() + 60 * 60 * 1000)],
[customer.id, token, new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)],
);
const resetLink = `${SITE}/account/reset-password?token=${token}`;
const orderList = orders.map(o => `<li style="margin:4px 0">${o.service_name} <span style="color:#888;font-family:monospace">(${o.order_number})</span></li>`).join("");
@ -41,7 +41,7 @@ await mailer.sendMail({
html: `<div style="font-family:Arial,sans-serif;max-width:560px;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.</p>
<p style="margin:18px 0 6px"><strong>1. Set your password to log in</strong> (expires in 60 minutes):</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. Sign your authorizations.</strong> We're sending you a separate signature request for each filing below. Each filing begins once you sign it - for the MCS-150 and USDOT reactivation we prepare the form and you review/sign before we submit to FMCSA.</p>
@ -49,7 +49,7 @@ await mailer.sendMail({
<p style="font-size:13px;color:#666">You can also track everything in your portal once you log in. 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} (expires 60 min). You'll also receive a signature request for each filing: ${orders.map(o => o.service_name + " (" + o.order_number + ")").join("; ")}. Each filing begins once you sign. Questions? 1-888-411-0383.`,
text: `Hi ${firstName}, set your password to log in: ${resetLink} (valid 7 days). You'll also receive a signature request for each filing: ${orders.map(o => o.service_name + " (" + o.order_number + ")").join("; ")}. Each filing begins once you sign. Questions? 1-888-411-0383.`,
});
console.log(`[rescue] login + signature note sent to ${EMAIL} (cc ${CC}) for ${orders.length} orders`);
await pool.end();

View file

@ -57,7 +57,7 @@ log(`customers row id=${customer.id} email=${customer.email} has_password=${cust
// 3a) password-set link (reuse the forgot-password token mechanism)
const token = crypto.randomBytes(32).toString("hex");
const expires = new Date(Date.now() + 60 * 60 * 1000); // 60 min
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
await pool.query(
`INSERT INTO password_reset_tokens (customer_id, token, expires_at) VALUES ($1,$2,$3)`,
[customer.id, token, expires],
@ -73,12 +73,12 @@ await mailer.sendMail({
<p>Hi ${firstName},</p>
<p>Thanks for your order. To finish setting up your account so you can log in to the
Performance West portal and track your filings, click below to choose a password.
This link expires in 60 minutes.</p>
This link is valid for 7 days.</p>
<p style="margin:24px 0"><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">Or paste this link into your browser:<br>${resetLink}</p>
<p style="font-size:13px;color:#666">Questions? Reply to this email or call 1-888-411-0383.</p>
</div>`,
text: `Hi ${firstName}, set your Performance West password to log in: ${resetLink} (expires in 60 minutes).`,
text: `Hi ${firstName}, set your Performance West password to log in: ${resetLink} (valid for 7 days).`,
});
log(`password-set link sent to ${NEW_EMAIL} (cc ${CC})`);