diff --git a/api/src/routes/portal-auth.ts b/api/src/routes/portal-auth.ts index b691647..b3c041b 100644 --- a/api/src/routes/portal-auth.ts +++ b/api/src/routes/portal-auth.ts @@ -16,7 +16,13 @@ import nodemailer from "nodemailer"; import { pool } from "../db.js"; const SITE_URL = process.env.SITE_URL || "https://performancewest.net"; -const RESET_TTL_MINUTES = 30; +// Password-RESET window for an existing account (security-sensitive): 2 hours. +const RESET_TTL_MINUTES = 120; +// Onboarding / first-password window for a NEW customer who hasn't engaged yet +// (e.g. set-password invites): 7 days, so the link doesn't expire before they +// get to it. These customers paid and just need to get in; a short window +// strands them. +export const ONBOARDING_TTL_MINUTES = 7 * 24 * 60; async function sendEmail(opts: { to: string; subject: string; html: string; text: string }) { const t = nodemailer.createTransport({ diff --git a/scripts/reissue-onboarding-links.mjs b/scripts/reissue-onboarding-links.mjs new file mode 100644 index 0000000..d918b16 --- /dev/null +++ b/scripts/reissue-onboarding-links.mjs @@ -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 "; + +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: `
+

Set your password and log in, ${firstName}

+

Apologies - the link we sent earlier expired too quickly. Here is a fresh one that stays valid for 7 days.

+

Set my password →

+

Or paste this link into your browser:
${link}

+

Once you're in, you can track your filings and complete any remaining intake. Questions? Reply here or call 1-888-411-0383.

+

Performance West Inc. · performancewest.net · 1-888-411-0383

+
`, + 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"); diff --git a/scripts/rescue-mark.mjs b/scripts/rescue-mark.mjs index f497bc8..2caff8f 100644 --- a/scripts/rescue-mark.mjs +++ b/scripts/rescue-mark.mjs @@ -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: `

You're all set, ${firstName}

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.

-

1. Set your password to log in (expires in 60 minutes):

+

1. Set your password to log in (valid for 7 days):

Set my password →

Or paste this link: ${resetLink}

2. Complete the short intake form so we have what we need to prepare your filing (about 2-5 minutes):

@@ -71,7 +71,7 @@ await mailer.sendMail({

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.

Performance West Inc. · performancewest.net · 1-888-411-0383

`, - 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(); diff --git a/scripts/rescue-mitchell-email.mjs b/scripts/rescue-mitchell-email.mjs index 044b110..3f361c9 100644 --- a/scripts/rescue-mitchell-email.mjs +++ b/scripts/rescue-mitchell-email.mjs @@ -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 => `
  • ${o.service_name} (${o.order_number})
  • `).join(""); @@ -41,7 +41,7 @@ await mailer.sendMail({ html: `

    You're all set, ${firstName}

    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.

    -

    1. Set your password to log in (expires in 60 minutes):

    +

    1. Set your password to log in (valid for 7 days):

    Set my password →

    Or paste this link: ${resetLink}

    2. Sign your authorizations. 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.

    @@ -49,7 +49,7 @@ await mailer.sendMail({

    You can also track everything in your portal once you log in. Questions? Reply here or call 1-888-411-0383.

    Performance West Inc. · performancewest.net · 1-888-411-0383

    `, - 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(); diff --git a/scripts/rescue-paul.mjs b/scripts/rescue-paul.mjs index 1cb8001..41d0c2a 100644 --- a/scripts/rescue-paul.mjs +++ b/scripts/rescue-paul.mjs @@ -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({

    Hi ${firstName},

    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.

    + This link is valid for 7 days.

    Set my password →

    Or paste this link into your browser:
    ${resetLink}

    Questions? Reply to this email or call 1-888-411-0383.

    `, - 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})`);