Diff -- /var/www/vhosts/3dshawn.com/crm.3dshawn.com/signup.cgi
Diff

/var/www/vhosts/3dshawn.com/crm.3dshawn.com/signup.cgi

added on local at 2026-07-01 15:03:02

Added
+236
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 6af26dd18dde
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# ContactForge — signup
4# Page body lives in TEMPLATES/cf_signup.html
5#
6# GET : show the signup form
7# POST : create user, mint session, set cookie, 302 to /dashboard.cgi
8# on failure, re-render the form with an error
9#======================================================================
10use strict;
11use warnings;
12
13use lib '/var/www/vhosts/3dshawn.com/crm.3dshawn.com';
14use CGI;
15use MODS::Template;
16use MODS::DBConnect;
17use MODS::Login;
18use MODS::ContactForge::Config;
19use MODS::ContactForge::Wrapper;
20use MODS::ContactForge::Billing;
21use MODS::ContactForge::EmailVerify;
22
23my $q = CGI->new;
24my $form = $q->Vars;
25my $auth = MODS::Login->new;
26my $wrap = MODS::ContactForge::Wrapper->new;
27my $tfile = MODS::Template->new;
28my $db = MODS::DBConnect->new;
29my $cfg = MODS::ContactForge::Config->new;
30my $bill = MODS::ContactForge::Billing->new;
31my $DB = $cfg->settings('database_name');
32
33$|=1;
34
35if (my $existing = $auth->login_verify()) {
36 print "Status: 302 Found\nLocation: /dashboard.cgi\n\n";
37 exit;
38}
39
40my $error_msg = '';
41my $email_val = '';
42my $name_val = '';
43# Default to the plan slug passed via ?plan= from the pricing cards.
44# Post pricing-v3: only pro|business are valid; coerce anything else to pro.
45my $plan_val = $form->{plan} || $form->{plan_tier} || 'pro';
46$plan_val =~ s/[^a-z0-9_\-]//gi;
47$plan_val = lc $plan_val;
48$plan_val = 'pro' unless ($plan_val eq 'pro' || $plan_val eq 'business');
49
50if (($ENV{REQUEST_METHOD} || '') eq 'POST') {
51 my $email = $form->{email} || '';
52 my $pass = $form->{password} || '';
53 my $name = $form->{display_name} || '';
54 my $plan = lc($form->{plan_tier} || $form->{plan} || 'pro');
55 $plan =~ s/[^a-z0-9_\-]//g;
56 $plan = 'pro' unless ($plan eq 'pro' || $plan eq 'business');
57
58 $email_val = _h($email);
59 $name_val = _h($name);
60 $plan_val = _h($plan);
61
62 if ($email !~ /^[^\@\s]+\@[^\@\s]+\.[^\@\s]+$/) {
63 $error_msg = 'Please enter a valid email address.';
64 } elsif (length($pass) < 8) {
65 $error_msg = 'Password must be at least 8 characters.';
66 } else {
67 my $user_id = $auth->signup($email, $pass, $name, $plan);
68 if ($user_id) {
69 # Post pricing-v3: every signup starts a 14-day free trial,
70 # capped at 250 contacts during the trial. trial_ends_at /
71 # trial_contacts_cap are USER-level state; the chosen tier
72 # is what they'll be charged after day 14. We also re-pin
73 # plan_tier in case auth->signup() left it on the column
74 # default (defensive -- harmless if signup already set it).
75 eval {
76 my $dbh_t = $db->db_connect();
77 my $cap = $cfg->settings('plan_trial_contacts_cap') || 250;
78 my $days = $cfg->settings('plan_trial_days') || 14;
79 my $uid_q = $user_id; $uid_q =~ s/[^0-9]//g;
80 my $cap_q = $cap; $cap_q =~ s/[^0-9]//g; $cap_q = 250 unless $cap_q;
81 my $days_q= $days; $days_q =~ s/[^0-9]//g; $days_q = 14 unless $days_q;
82 my $plan_safe = $plan; $plan_safe =~ s/[^a-z]//g;
83 $db->db_readwrite($dbh_t, qq~
84 UPDATE `${DB}`.users
85 SET plan_tier='$plan_safe',
86 trial_ends_at=DATE_ADD(NOW(), INTERVAL $days_q DAY),
87 trial_contacts_cap=$cap_q
88 WHERE id='$uid_q'
89 ~, $ENV{SCRIPT_NAME}, __LINE__);
90 $db->db_disconnect($dbh_t);
91 };
92
93 # Seed sample CRM data so the dashboard isn't empty on first login.
94 eval {
95 require MODS::ContactForge::Onboarding;
96 my $dbh3 = $db->db_connect();
97 MODS::ContactForge::Onboarding->new->seed_sample_data($db, $dbh3, $DB, $user_id);
98 $db->db_disconnect($dbh3);
99 };
100
101 # Fire-and-forget the verification email. The user is
102 # signed in immediately either way (we don't gate the
103 # platform behind verification); a bounce or mailer-down
104 # condition just means they'll see a "Resend verification"
105 # button on /profile.cgi until they confirm. Wrapped in
106 # eval so a SMTP outage never blocks signup.
107 eval {
108 my $dbh2 = $db->db_connect();
109 MODS::ContactForge::EmailVerify->send_for_user($db, $dbh2, $DB, {
110 id => $user_id,
111 email => $email,
112 display_name => $name,
113 });
114 $db->db_disconnect($dbh2);
115 };
116
117 my ($userinfo, $token) = $auth->login_exec($email, $pass);
118 if ($userinfo && $token) {
119 print $auth->set_cookie_header($token);
120 # Brand-new accounts land on /dashboard.cgi -- the
121 # in-app home, with KPIs + tiles. Sample data seeding
122 # above ensures the page isn't empty on day zero.
123 print "Status: 302 Found\nLocation: /dashboard.cgi\n\n";
124 exit;
125 }
126 $error_msg = 'Account created — please sign in.';
127 } else {
128 $error_msg = 'That email is already in use, or an account error occurred.';
129 }
130 }
131}
132
133print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
134
135# Build the plan <option> list from billing_plans (admin-managed). One
136# entry per unique tier; we prefer the monthly price for the label.
137# Falls back to a built-in set when the schema isn't migrated.
138my @plan_options = _build_plan_options($db, $bill, $plan_val);
139
140my $tvars = {
141 error_msg => $error_msg,
142 email_val => $email_val,
143 name_val => $name_val,
144 plan_options => \@plan_options,
145};
146
147my $body = join('', $tfile->template('cf_signup.html', $tvars, undef));
148
149
150# === Maintenance lockdown overlay (auto-injected 2026-06-23) ===
151# When maintenance_locked=1, render a modal over the login form.
152# X-close stays visible so an admin can sign in. Non-admin login
153# attempts fail and the page reloads, restoring the overlay.
154{
155 my $_mcfg = MODS::ContactForge::Config->new;
156 if ($_mcfg->settings('maintenance_locked')) {
157 my $_mmsg = $_mcfg->settings('maintenance_message') || '<p>The system is in maintenance mode.</p>';
158 my $_overlay = qq{<div id="maint-overlay" style="position:fixed;inset:0;background:rgba(0,0,0,0.88);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);z-index:9999;display:flex;align-items:center;justify-content:center;padding:24px"><div style="position:relative;background:#0f0f0f;border:1px solid rgba(217,119,6,0.6);border-radius:12px;max-width:680px;width:100%;padding:48px 40px;box-shadow:0 30px 90px rgba(0,0,0,0.6),0 0 0 1px rgba(217,119,6,0.15)"><button type="button" aria-label="Close" onclick="document.getElementById('maint-overlay').style.display='none'" style="position:absolute;top:14px;right:14px;width:36px;height:36px;background:transparent;border:1px solid rgba(255,255,255,0.12);color:#aaa;font-size:22px;line-height:1;border-radius:8px;cursor:pointer">&times;</button>$_mmsg</div><script>document.addEventListener('keydown',function(e){if(e.key==='Escape'){var o=document.getElementById('maint-overlay');if(o)o.style.display='none';}});</script></div>};
159 $body = $_overlay . $body;
160 }
161}
162# === End maintenance lockdown overlay ===
163$wrap->render({
164 userinfo => undef,
165 title => 'Start your trial',
166 body => $body,
167});
168
169#----------------------------------------------------------------------
170sub _h {
171 my $s = shift // '';
172 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
173 $s =~ s/"/&quot;/g; $s =~ s/'/&#39;/g;
174 return $s;
175}
176
177# Build the <option> list for the plan dropdown. Reads billing_plans
178# (preferring the monthly row of each tier for the label price), and
179# pre-marks the matching tier with selected_attr. Connection-safe:
180# opens its own connection so signup pages on databases without the
181# billing schema still render via the fallback list.
182sub _build_plan_options {
183 my ($db, $bill, $current_tier) = @_;
184 $current_tier = '' unless defined $current_tier;
185 my @out;
186
187 my $dbh = eval { $db->db_connect() };
188 if ($dbh && $bill->schema_ready($db, $dbh, $DB)) {
189 my @rows = $bill->list_plans($db, $dbh, $DB);
190 my %seen_tier;
191 # Prefer monthly row per tier (annual is implicit on the pricing card).
192 my @monthly = grep { ($_->{cadence}||'') eq 'monthly' } @rows;
193 my @backup = grep { ($_->{cadence}||'') ne 'monthly' } @rows;
194 foreach my $r (@monthly, @backup) {
195 next if $seen_tier{ $r->{tier} }++;
196 my $tier = $r->{tier};
197 my $label = ($r->{display_name} || ucfirst($tier))
198 . ' &mdash; '
199 . _signup_price_label($r->{price_cents})
200 . ($r->{tagline} ? (' &middot; ' . $r->{tagline}) : '');
201 push @out, {
202 value => $tier,
203 label => $label,
204 selected_attr => ($tier eq $current_tier) ? ' selected' : '',
205 };
206 }
207 $db->db_disconnect($dbh);
208 return @out if scalar @out;
209 }
210 $db->db_disconnect($dbh) if $dbh;
211
212 # Fallback for un-migrated databases.
213 # Post pricing-v3 (2026-06): 2 paid tiers; every signup starts on a
214 # 14-day free trial regardless of which tier they pick.
215 foreach my $r (
216 { v=>'pro', l=>'Pro &mdash; $29/mo, 25k contacts, 5 seats (14-day free trial)' },
217 { v=>'business', l=>'Business &mdash; $99/mo, unlimited contacts/seats + API + SSO (14-day free trial)' },
218 ) {
219 push @out, {
220 value => $r->{v},
221 label => $r->{l},
222 selected_attr => ($r->{v} eq $current_tier) ? ' selected' : '',
223 };
224 }
225 return @out;
226}
227
228sub _signup_price_label {
229 my ($cents) = @_;
230 $cents = 0 unless defined $cents;
231 return 'free forever' if $cents == 0;
232 if ($cents % 100 == 0) {
233 return '$' . int($cents / 100) . '/mo';
234 }
235 return '$' . sprintf('%.2f', $cents / 100) . '/mo';
236}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help