This commit is contained in:
parent
6e673ed514
commit
219493e1b0
@ -1 +1 @@
|
||||
33fb91d4b60b7f14d236f83e44c9db42fa1d440f-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
|
||||
767a2e51259e707655c80d6449afa93abf982fec-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
|
||||
|
@ -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
|
||||
];
|
||||
|
||||
|
||||
|
@ -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 = `<?xml version="1.0" encoding="UTF-8"?><Response><Message>AptivaAI: Help with SMS. Email admin@aptivaai.com. Msg&Data rates may apply. Reply STOP to cancel.</Message></Response>`;
|
||||
const twiml = `<?xml version="1.0" encoding="UTF-8"?><Response><Message>AptivaAI: Help with SMS. Email support@aptivaai.com. Msg&Data rates may apply. Reply STOP to cancel.</Message></Response>`;
|
||||
return res.type('text/xml').send(twiml);
|
||||
}
|
||||
return res.type('text/xml').send(`<?xml version="1.0" encoding="UTF-8"?><Response/>`);
|
||||
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
21
nginx.conf
21
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 "";
|
||||
}
|
||||
}
|
||||
|
||||
########################################################################
|
||||
|
BIN
public/a2p/assets/opt-in-checkbox.png
Normal file
BIN
public/a2p/assets/opt-in-checkbox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
BIN
public/a2p/assets/reminders-toggle.png
Normal file
BIN
public/a2p/assets/reminders-toggle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
public/a2p/assets/sms-terms.png
Normal file
BIN
public/a2p/assets/sms-terms.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
public/a2p/assets/verify-screen.png
Normal file
BIN
public/a2p/assets/verify-screen.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<title>AptivaAI – A2P 10DLC Campaign Verification</title>
|
||||
<title>AptivaAI - A2P 10DLC Campaign Verification</title>
|
||||
<style>
|
||||
:root { --fg:#111; --muted:#555; --link:#1d4ed8; }
|
||||
body { font:16px/1.6 system-ui,Segoe UI,Roboto,Arial,sans-serif; color:var(--fg);
|
||||
@ -23,7 +23,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>AptivaAI – A2P 10DLC Campaign Verification</h1>
|
||||
<h1>AptivaAI - A2P 10DLC Campaign Verification</h1>
|
||||
<div class="muted">Last updated: Sep 13, 2025</div>
|
||||
|
||||
<h2>Program & Use Case</h2>
|
||||
@ -38,6 +38,8 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p><strong>Originating number(s):</strong> +1 478-500-3955</p>
|
||||
|
||||
<h2>Call to Action (CTA) & Consent</h2>
|
||||
<p>Users opt in <em>inside their account</em> by entering a mobile number, checking a non-prechecked consent box linking to our
|
||||
<a href="/sms" target="_blank" rel="noreferrer">SMS Terms</a>, <a href="/legal/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>, and
|
||||
@ -66,8 +68,10 @@
|
||||
<p>All samples include brand and opt-out/help language; no shortened links or phone numbers in 2FA content.</p>
|
||||
<ul>
|
||||
<li><code>AptivaAI code: 123456. Expires in 10 minutes. Reply STOP to cancel, HELP for help.</code></li>
|
||||
<li><code>AptivaAI sign-in code: 654321. If you didn’t request this, change your password.</code></li>
|
||||
<li><code>AptivaAI sign-in code: 654321. If you didn't request this, change your password.</code></li>
|
||||
<li><code>AptivaAI password reset code: 112233. Expires in 10 minutes. Reply STOP to cancel.</code></li>
|
||||
<li><code>AptivaAI: SMS for verification & security alerts enabled. Message & Data rates may apply. Reply HELP for help, STOP to cancel. Frequency varies.</code></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -91,12 +95,46 @@
|
||||
<img src="/a2p/assets/opt-in-checkbox.png" alt="Consent checkbox and copy" />
|
||||
<figcaption>Consent checkbox (non-prechecked) linking to policies</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="/a2p/assets/otp-sms.png" alt="Example 2FA SMS on device" />
|
||||
<figcaption>Example OTP SMS</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<p class="muted">Carriers are not liable for delayed or undelivered messages.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
<h2>Reminders (Account Notifications)</h2>
|
||||
|
||||
<div class="card">
|
||||
<p><strong>Program:</strong> AptivaAI Reminders (account notifications; non-marketing)</p>
|
||||
<p><strong>Originating number(s):</strong> +1 678-710-3755</p>
|
||||
<ul>
|
||||
<li><strong>What we send:</strong> task & milestone reminders the user enables in Settings.</li>
|
||||
<li><strong>Opt-out:</strong> Reply <strong>STOP</strong> to cancel; <strong>HELP</strong> for help.</li>
|
||||
<li><strong>Fees:</strong> Msg & data rates may apply.</li>
|
||||
<li><strong>Frequency:</strong> varies by user settings.</li>
|
||||
<li><strong>Consent:</strong> not a condition of purchase or service.</li>
|
||||
<li><strong>Policies:</strong>
|
||||
<a href="/sms" target="_blank" rel="noreferrer">SMS Terms</a> •
|
||||
<a href="/legal/privacy" target="_blank" rel="noreferrer">Privacy Policy</a> •
|
||||
<a href="/legal/terms" target="_blank" rel="noreferrer">Terms of Service</a>
|
||||
</li>
|
||||
<li><strong>Contact:</strong> <a href="mailto:support@aptivaai.com">support@aptivaai.com</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grid" style="margin-top:10px">
|
||||
<figure>
|
||||
<img src="/a2p/assets/reminders-toggle.png"
|
||||
alt="In-app Reminders toggle with non-prechecked consent and policy links">
|
||||
<figcaption>In-app CTA: non-prechecked consent for SMS reminders inside the account.</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<h3>Sample Messages</h3>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li><code>AptivaAI reminder: “[Task name]” is due tomorrow. Reply STOP to cancel, HELP for help.</code></li>
|
||||
<li><code>AptivaAI reminder: “[Milestone]” starts at [3:00 PM] today. Reply STOP to cancel.</code></li>
|
||||
<li><code>AptivaAI reminder: Weekly goal check-in. Reply STOP to cancel, HELP for help.</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -10,5 +10,7 @@
|
||||
</style>
|
||||
<h1>Privacy Policy</h1>
|
||||
<p>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 <strong>STOP</strong>.</p>
|
||||
<p>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.</p>
|
||||
|
||||
<p>Contact: <a href="mailto:support@aptivaai.com">support@aptivaai.com</a></p>
|
||||
<p><small>Last updated: Sep 13, 2025</small></p>
|
||||
|
@ -15,6 +15,11 @@
|
||||
<li><strong>Opt-out:</strong> Reply <strong>STOP</strong> to cancel. Reply <strong>HELP</strong> for help.</li>
|
||||
<li><strong>Support:</strong> <a href="mailto:support@aptivaai.com">support@aptivaai.com</a></li>
|
||||
<li><strong>Carriers:</strong> Not liable for delayed or undelivered messages.</li>
|
||||
<p><strong>Note:</strong> Consent is not a condition of purchase or service, and you can disable SMS in your account at any time.</p>
|
||||
|
||||
</ul>
|
||||
<p>See our <a href="/legal/privacy">Privacy Policy</a> and <a href="/legal/terms">Terms of Service</a>.</p>
|
||||
<p><small>Last updated: Sep 13, 2025</small></p>
|
||||
|
||||
<p><strong>Reminders Program (Account Notifications):</strong> Non-marketing reminders you enable in your account (frequency varies). Reply <strong>STOP</strong> to cancel, <strong>HELP</strong> for help. Consent is not a condition of purchase or service.</p>
|
||||
|
||||
|
@ -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 */}
|
||||
<div className="mt-4">
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Mobile (E.164)</label>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Mobile</label>
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="+15551234567"
|
||||
@ -293,14 +298,27 @@ function UserProfile() {
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<label className="mt-2 inline-flex items-center gap-2 text-sm">
|
||||
<label className="flex items-start gap-2 text-xs text-gray-700 leading-5 mt-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={smsOptIn}
|
||||
onChange={(e) => setSmsOptIn(e.target.checked)}
|
||||
/>
|
||||
I agree to receive SMS updates.
|
||||
type="checkbox"
|
||||
checked={smsRemindersOptIn}
|
||||
onChange={e => setSmsRemindersOptIn(e.target.checked)}
|
||||
disabled={!phoneVerifiedAt} // require a verified phone
|
||||
/>
|
||||
<span>
|
||||
Enable SMS reminders (non-marketing). Message frequency varies. Msg & data rates may apply.
|
||||
Reply <strong>STOP</strong> to cancel, <strong>HELP</strong> for help.
|
||||
Consent is not a condition of purchase or service. See{' '}
|
||||
<a className="underline" href="/sms" target="_blank" rel="noreferrer">SMS Terms</a>,{' '}
|
||||
<a className="underline" href="/legal/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>, and{' '}
|
||||
<a className="underline" href="/legal/terms" target="_blank" rel="noreferrer">Terms</a>.
|
||||
</span>
|
||||
</label>
|
||||
{!phoneVerifiedAt && (
|
||||
<div className="text-[11px] text-gray-500 mt-1">
|
||||
Verify your number on the <a className="underline" href="/verify">Verify</a> page before enabling reminders.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Career Situation */}
|
||||
|
@ -126,11 +126,12 @@ export default function Verify() {
|
||||
onChange={e => setSmsConsent(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="sms-consent" className="text-xs text-gray-700 leading-5">
|
||||
By requesting a code, you agree to the{' '}
|
||||
By requesting a code, you agree to receive one-time texts from AptivaAI for account verification and security alerts.
|
||||
Message frequency varies. Msg & data rates may apply. Reply <strong>STOP</strong> to opt out, <strong>HELP</strong> for help.
|
||||
Consent is not a condition of purchase or service. See the{' '}
|
||||
<a className="underline" href="/sms" target="_blank" rel="noreferrer">SMS Terms</a>,{' '}
|
||||
<a className="underline" href="/legal/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>,{' '}
|
||||
and <a className="underline" href="/legal/terms" target="_blank" rel="noreferrer">Terms</a>.{' '}
|
||||
Reply STOP to opt out. Msg & data rates may apply.
|
||||
<a className="underline" href="/legal/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>, and{' '}
|
||||
<a className="underline" href="/legal/terms" target="_blank" rel="noreferrer">Terms</a>.
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user