diff --git a/.build.hash b/.build.hash index b81397b..97c76bb 100644 --- a/.build.hash +++ b/.build.hash @@ -1 +1 @@ -33fb91d4b60b7f14d236f83e44c9db42fa1d440f-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b +767a2e51259e707655c80d6449afa93abf982fec-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b diff --git a/backend/server1.js b/backend/server1.js index 9ea454e..efacfdb 100755 --- a/backend/server1.js +++ b/backend/server1.js @@ -1024,7 +1024,8 @@ app.post('/api/user-profile', requireAuth, async (req, res) => { career_priorities, career_list, phone_e164, - sms_opt_in + sms_opt_in, + sms_reminders_opt_in } = req.body; try { @@ -1069,6 +1070,10 @@ app.post('/api/user-profile', requireAuth, async (req, res) => { ? (sms_opt_in ? 1 : 0) : (existing?.sms_opt_in ?? 0); + const smsRemindersFinal = (typeof sms_reminders_opt_in === 'boolean') + ? (sms_reminders_opt_in ? 1 : 0) + : (existing?.sms_reminders_opt_in ?? 0); + if (existing) { const updateQuery = ` UPDATE user_profile @@ -1086,7 +1091,14 @@ app.post('/api/user-profile', requireAuth, async (req, res) => { career_priorities = ?, career_list = ?, phone_e164 = ?, - sms_opt_in = ? + sms_opt_in = ?, + sms_reminders_opt_in = ? + sms_reminders_opt_in_at = + CASE + WHEN ? = 1 AND (sms_reminders_opt_in IS NULL OR sms_reminders_opt_in = 0) + THEN UTC_TIMESTAMP() + ELSE sms_reminders_opt_in_at + END WHERE id = ? `; const params = [ @@ -1105,6 +1117,7 @@ app.post('/api/user-profile', requireAuth, async (req, res) => { finalCareerList, phoneFinal, smsOptFinal, + smsRemindersFinal, profileId ]; @@ -1116,17 +1129,17 @@ app.post('/api/user-profile', requireAuth, async (req, res) => { INSERT INTO user_profile (id, username, firstname, lastname, email, email_lookup, zipcode, state, area, career_situation, interest_inventory_answers, riasec_scores, - career_priorities, career_list, phone_e164, sms_opt_in) + career_priorities, career_list, phone_e164, sms_opt_in, sms_reminders_opt_in) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, - ?, ?, ?, ?) + ?, ?, ?, ?, ?) `; const params = [ profileId, finalUserName, firstName, lastName, - encEmail, // <-- was emailNorm + encEmail, emailLookupVal, zipCode, state, @@ -1137,7 +1150,8 @@ app.post('/api/user-profile', requireAuth, async (req, res) => { finalCareerPriorities, finalCareerList, phoneFinal, - smsOptFinal + smsOptFinal, + smsRemindersFinal ]; diff --git a/backend/server3.js b/backend/server3.js index afda958..b879e0e 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -19,7 +19,7 @@ import pkg from 'pdfjs-dist'; import pool from './config/mysqlPool.js'; import { v4 as uuid } from 'uuid'; import { decrypt } from './shared/crypto/encryption.js' - +import crypto from 'crypto'; import OpenAI from 'openai'; import Fuse from 'fuse.js'; import Stripe from 'stripe'; @@ -690,7 +690,7 @@ const twilioForm = express.urlencoded({ extended: false }); app.post('/api/auth/sms/inbound', twilioForm, async (req, res) => { const body = String(req.body?.Body || '').trim().toUpperCase(); if (body === 'HELP') { - const twiml = `AptivaAI: Help with SMS. Email admin@aptivaai.com. Msg&Data rates may apply. Reply STOP to cancel.`; + const twiml = `AptivaAI: Help with SMS. Email support@aptivaai.com. Msg&Data rates may apply. Reply STOP to cancel.`; return res.type('text/xml').send(twiml); } return res.type('text/xml').send(``); @@ -4024,11 +4024,11 @@ app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { /* ───────────────── SMS reminder ───────────────── */ if (due_date) { // only if task has a due date - const [[profile]] = await pool.query( - 'SELECT phone_e164, sms_opt_in FROM user_profile WHERE id = ?', - [req.id] - ); - if (profile?.sms_opt_in && profile.phone_e164) { + const [[profile]] = await pool.query( + 'SELECT phone_e164, phone_verified_at, sms_reminders_opt_in FROM user_profile WHERE id = ?', + [req.id] + ); + if (profile?.sms_reminders_opt_in && profile.phone_verified_at && profile.phone_e164) { await createReminder({ userId : req.id, phone : profile.phone_e164, diff --git a/migrate_encrypted_columns.sql b/migrate_encrypted_columns.sql index c7fb41f..7e405f6 100644 --- a/migrate_encrypted_columns.sql +++ b/migrate_encrypted_columns.sql @@ -252,3 +252,8 @@ mysqldump \ ALTER TABLE user_profile ADD COLUMN email_verified_at DATETIME NULL AFTER email_lookup, ADD COLUMN phone_verified_at DATETIME NULL AFTER phone_e164; + + ALTER TABLE user_profile + ADD COLUMN sms_reminders_opt_in TINYINT(1) NOT NULL DEFAULT 0, + ADD COLUMN sms_reminders_opt_in_at DATETIME NULL; + diff --git a/nginx.conf b/nginx.conf index 464f9a4..16b13b5 100644 --- a/nginx.conf +++ b/nginx.conf @@ -30,6 +30,13 @@ http { listen [::]:80; server_name dev1.aptivaai.com; return 301 https://$host$request_uri; + + location ^~ /api/auth/sms/ { + proxy_pass http://backend5002; # server3 + proxy_http_version 1.1; + proxy_set_header Connection ""; +} + } ######################################################################## @@ -171,6 +178,13 @@ http { location ^~ /api/auth/ { limit_conn perip 5; limit_req zone=reqperip burst=10 nodelay; proxy_pass http://backend5000; } + + location ^~ /api/auth/sms/ { + proxy_pass http://backend5002; # server3 + proxy_http_version 1.1; + proxy_set_header Connection ""; + } + location = /api/user-profile { limit_conn perip 5; limit_req zone=reqperip burst=10 nodelay; proxy_pass http://backend5000; } @@ -208,6 +222,13 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; } + + # Twilio webhooks live in server3 (backend5002) + location ^~ /api/auth/sms/ { + proxy_pass http://backend5002; + proxy_http_version 1.1; + proxy_set_header Connection ""; + } } ######################################################################## diff --git a/public/a2p/assets/opt-in-checkbox.png b/public/a2p/assets/opt-in-checkbox.png new file mode 100644 index 0000000..a33e473 Binary files /dev/null and b/public/a2p/assets/opt-in-checkbox.png differ diff --git a/public/a2p/assets/reminders-toggle.png b/public/a2p/assets/reminders-toggle.png new file mode 100644 index 0000000..97bffe1 Binary files /dev/null and b/public/a2p/assets/reminders-toggle.png differ diff --git a/public/a2p/assets/sms-terms.png b/public/a2p/assets/sms-terms.png new file mode 100644 index 0000000..a8f1aa8 Binary files /dev/null and b/public/a2p/assets/sms-terms.png differ diff --git a/public/a2p/assets/verify-screen.png b/public/a2p/assets/verify-screen.png new file mode 100644 index 0000000..4920d12 Binary files /dev/null and b/public/a2p/assets/verify-screen.png differ diff --git a/public/a2p/index.html b/public/a2p/index.html index cd4cc51..9f3c385 100644 --- a/public/a2p/index.html +++ b/public/a2p/index.html @@ -4,7 +4,7 @@ - AptivaAI – A2P 10DLC Campaign Verification + AptivaAI - A2P 10DLC Campaign Verification -

AptivaAI – A2P 10DLC Campaign Verification

+

AptivaAI - A2P 10DLC Campaign Verification

Last updated: Sep 13, 2025

Program & Use Case

@@ -38,6 +38,8 @@ +

Originating number(s): +1 478-500-3955

+

Call to Action (CTA) & Consent

Users opt in inside their account by entering a mobile number, checking a non-prechecked consent box linking to our SMS Terms, Privacy Policy, and @@ -66,8 +68,10 @@

All samples include brand and opt-out/help language; no shortened links or phone numbers in 2FA content.

@@ -91,12 +95,46 @@ Consent checkbox and copy
Consent checkbox (non-prechecked) linking to policies
-
- Example 2FA SMS on device -
Example OTP SMS
-

Carriers are not liable for delayed or undelivered messages.

+ + +

Reminders (Account Notifications)

+ +
+

Program: AptivaAI Reminders (account notifications; non-marketing)

+

Originating number(s): +1 678-710-3755

+ +
+ +
+
+ In-app Reminders toggle with non-prechecked consent and policy links +
In-app CTA: non-prechecked consent for SMS reminders inside the account.
+
+
+ +

Sample Messages

+
+ +
diff --git a/public/legal/privacy/index.html b/public/legal/privacy/index.html index ac5d9a9..31829ba 100644 --- a/public/legal/privacy/index.html +++ b/public/legal/privacy/index.html @@ -10,5 +10,7 @@

Privacy Policy

We collect account information (name, email), an optional phone number to enable SMS, and usage data. We use this information to provide and secure the service, including sending SMS you enable (e.g., verification codes). We share data with processors under contracts that limit use to our instructions. You can opt out of SMS by replying STOP.

+

We do not share mobile information with third parties or affiliates for marketing or promotional purposes. SMS opt-in data and consent are not shared with any third parties.

+

Contact: support@aptivaai.com

Last updated: Sep 13, 2025

diff --git a/public/sms/index.html b/public/sms/index.html index 67ff8ad..e915691 100644 --- a/public/sms/index.html +++ b/public/sms/index.html @@ -15,6 +15,11 @@
  • Opt-out: Reply STOP to cancel. Reply HELP for help.
  • Support: support@aptivaai.com
  • Carriers: Not liable for delayed or undelivered messages.
  • +

    Note: Consent is not a condition of purchase or service, and you can disable SMS in your account at any time.

    +

    See our Privacy Policy and Terms of Service.

    Last updated: Sep 13, 2025

    + +

    Reminders Program (Account Notifications): Non-marketing reminders you enable in your account (frequency varies). Reply STOP to cancel, HELP for help. Consent is not a condition of purchase or service.

    + diff --git a/src/components/UserProfile.js b/src/components/UserProfile.js index 9a2a5c7..750232a 100644 --- a/src/components/UserProfile.js +++ b/src/components/UserProfile.js @@ -14,8 +14,9 @@ function UserProfile() { const [loadingAreas, setLoadingAreas] = useState(false); const [phoneE164, setPhoneE164] = useState(''); - const [smsOptIn, setSmsOptIn] = useState(false); + const [smsRemindersOptIn, setSmsRemindersOptIn] = useState(false); const [showChangePw, setShowChangePw] = useState(false); + const [phoneVerifiedAt, setPhoneVerifiedAt] = useState(null); // Subscription state const [sub, setSub] = useState(null); @@ -77,12 +78,14 @@ function UserProfile() { useEffect(() => { (async () => { try { - const res = await authFetch('/api/user-profile?fields=' + - [ - 'firstname','lastname','email', - 'zipcode','state','area','career_situation', - 'phone_e164','sms_opt_in' - ].join(','), + const res = await authFetch( + '/api/user-profile?fields=' + + [ + 'firstname','lastname','email', + 'zipcode','state','area','career_situation', + 'phone_e164','sms_opt_in', + 'phone_verified_at','sms_reminders_opt_in' // may be absent if BE not updated yet + ].join(','), { method: 'GET' } ); if (!res || !res.ok) return; @@ -96,7 +99,8 @@ function UserProfile() { setSelectedArea(data.area || ''); setCareerSituation(data.career_situation || ''); setPhoneE164(data.phone_e164 || ''); - setSmsOptIn(!!data.sms_opt_in); + setSmsRemindersOptIn(!!data.sms_reminders_opt_in); // falls back to false if field not returned + setPhoneVerifiedAt(data.phone_verified_at || null); if (data.state) { setLoadingAreas(true); @@ -150,7 +154,8 @@ function UserProfile() { area: selectedArea, careerSituation, phone_e164: phoneE164 || null, - sms_opt_in: !!smsOptIn, + sms_reminders_opt_in: !!smsRemindersOptIn, + }; try { @@ -285,7 +290,7 @@ function UserProfile() { {/* Phone + SMS */}
    - + setPhoneE164(e.target.value)} className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:border-blue-600 focus:outline-none" /> -
    + {!phoneVerifiedAt && ( +
    + Verify your number on the Verify page before enabling reminders. +
    + )} + {/* Career Situation */}
    diff --git a/src/components/Verify.js b/src/components/Verify.js index 8bd46f9..8e0ef07 100644 --- a/src/components/Verify.js +++ b/src/components/Verify.js @@ -126,11 +126,12 @@ export default function Verify() { onChange={e => setSmsConsent(e.target.checked)} />