Diff -- /var/www/vhosts/webstls.com/httpdocs/signup.cgi
Diff

/var/www/vhosts/webstls.com/httpdocs/signup.cgi

added on WebSTLs (webstls.com) at 2026-07-01 22:26:46

Added
+215
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 0625b4dee4fa
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# WebSTLs — signup
4# Page body lives in TEMPLATES/webstls_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/webstls.com/httpdocs';
14use CGI;
15use MODS::Template;
16use MODS::DBConnect;
17use MODS::Login;
18use MODS::WebSTLs::Config;
19use MODS::WebSTLs::Wrapper;
20use MODS::WebSTLs::Billing;
21use MODS::WebSTLs::EmailVerify;
22
23my $q = CGI->new;
24my $form = $q->Vars;
25if (defined $form->{company_website} && length($form->{company_website})) {
26 print "Content-Type: text/html; charset=utf-8\n\n";
27 print '<!doctype html><html><head><title>Check your email</title></head><body style="font-family:sans-serif;padding:40px;text-align:center"><h1>Check your email</h1><p>We sent a verification link to your address. Click it to finish setting up your account.</p></body></html>';
28 exit;
29}
30
31my $auth = MODS::Login->new;
32my $wrap = MODS::WebSTLs::Wrapper->new;
33my $tfile = MODS::Template->new;
34my $db = MODS::DBConnect->new;
35my $cfg = MODS::WebSTLs::Config->new;
36my $bill = MODS::WebSTLs::Billing->new;
37my $DB = $cfg->settings('database_name');
38
39$|=1;
40
41if (my $existing = $auth->login_verify()) {
42 print "Status: 302 Found\nLocation: /dashboard.cgi\n\n";
43 exit;
44}
45
46my $error_msg = '';
47my $email_val = '';
48my $name_val = '';
49# Default to the plan slug passed via ?plan= from the pricing cards.
50my $plan_val = $form->{plan} || $form->{plan_tier} || 'pro';
51$plan_val =~ s/[^a-z0-9_\-]//gi;
52$plan_val = lc $plan_val;
53
54if (($ENV{REQUEST_METHOD} || '') eq 'POST') {
55 my $email = $form->{email} || '';
56 my $pass = $form->{password} || '';
57 my $name = $form->{display_name} || '';
58 my $plan = $form->{plan_tier} || 'free';
59
60 $email_val = _h($email);
61 $name_val = _h($name);
62 $plan_val = _h($plan);
63
64 if ($email !~ /^[^\@\s]+\@[^\@\s]+\.[^\@\s]+$/) {
65 $error_msg = 'Please enter a valid email address.';
66 } elsif (length($pass) < 8) {
67 $error_msg = 'Password must be at least 8 characters.';
68 } else {
69 my $user_id = $auth->signup($email, $pass, $name, $plan);
70 if ($user_id) {
71 # Seed sample storefront + 4 sample STL products so the dashboard isn't empty.
72 eval {
73 require MODS::WebSTLs::Onboarding;
74 my $dbh3 = $db->db_connect();
75 MODS::WebSTLs::Onboarding->new->seed_sample_data($db, $dbh3, $DB, $user_id);
76 $db->db_disconnect($dbh3);
77 };
78
79 # Fire-and-forget the verification email. The user is
80 # signed in immediately either way (we don't gate the
81 # platform behind verification); a bounce or mailer-down
82 # condition just means they'll see a "Resend verification"
83 # button on /profile.cgi until they confirm. Wrapped in
84 # eval so a SMTP outage never blocks signup.
85 eval {
86 my $dbh2 = $db->db_connect();
87 MODS::WebSTLs::EmailVerify->send_for_user($db, $dbh2, $DB, {
88 id => $user_id,
89 email => $email,
90 display_name => $name,
91 });
92 $db->db_disconnect($dbh2);
93 };
94
95 my ($userinfo, $token) = $auth->login_exec($email, $pass);
96 if ($userinfo && $token) {
97 print $auth->set_cookie_header($token);
98 # Brand-new accounts land on /dashboard.cgi -- the
99 # getting-started console with checklist + plan summary
100 # is the dashboard. Same surface for day zero AND ongoing
101 # use, matching the PTMatrix URL convention.
102 print "Status: 302 Found\nLocation: /dashboard.cgi\n\n";
103 exit;
104 }
105 $error_msg = 'Account created — please sign in.';
106 } else {
107 $error_msg = 'That email is already in use, or an account error occurred.';
108 }
109 }
110}
111
112print "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";
113
114# Build the plan <option> list from billing_plans (admin-managed). One
115# entry per unique tier; we prefer the monthly price for the label.
116# Falls back to a built-in set when the schema isn't migrated.
117my @plan_options = _build_plan_options($db, $bill, $plan_val);
118
119my $tvars = {
120 error_msg => $error_msg,
121 email_val => $email_val,
122 name_val => $name_val,
123 plan_options => \@plan_options,
124};
125
126my $body = join('', $tfile->template('webstls_signup.html', $tvars, undef));
127
128
129# === Maintenance lockdown overlay (auto-injected 2026-06-23) ===
130# When maintenance_locked=1, render a modal over the login form.
131# X-close stays visible so an admin can sign in. Non-admin login
132# attempts fail and the page reloads, restoring the overlay.
133{
134 my $_mcfg = MODS::WebSTLs::Config->new;
135 if ($_mcfg->settings('maintenance_locked')) {
136 my $_mmsg = $_mcfg->settings('maintenance_message') || '<p>The system is in maintenance mode.</p>';
137 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>};
138 $body = $_overlay . $body;
139 }
140}
141# === End maintenance lockdown overlay ===
142$wrap->render({
143 userinfo => undef,
144 title => 'Start your trial',
145 body => $body,
146});
147
148#----------------------------------------------------------------------
149sub _h {
150 my $s = shift // '';
151 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
152 $s =~ s/"/&quot;/g; $s =~ s/'/&#39;/g;
153 return $s;
154}
155
156# Build the <option> list for the plan dropdown. Reads billing_plans
157# (preferring the monthly row of each tier for the label price), and
158# pre-marks the matching tier with selected_attr. Connection-safe:
159# opens its own connection so signup pages on databases without the
160# billing schema still render via the fallback list.
161sub _build_plan_options {
162 my ($db, $bill, $current_tier) = @_;
163 $current_tier = '' unless defined $current_tier;
164 my @out;
165
166 my $dbh = eval { $db->db_connect() };
167 if ($dbh && $bill->schema_ready($db, $dbh, $DB)) {
168 my @rows = $bill->list_plans($db, $dbh, $DB);
169 my %seen_tier;
170 # Prefer monthly row per tier (annual is implicit on the pricing card).
171 my @monthly = grep { ($_->{cadence}||'') eq 'monthly' } @rows;
172 my @backup = grep { ($_->{cadence}||'') ne 'monthly' } @rows;
173 foreach my $r (@monthly, @backup) {
174 next if $seen_tier{ $r->{tier} }++;
175 my $tier = $r->{tier};
176 my $label = ($r->{display_name} || ucfirst($tier))
177 . ' &mdash; '
178 . _signup_price_label($r->{price_cents})
179 . ($r->{tagline} ? (' &middot; ' . $r->{tagline}) : '');
180 push @out, {
181 value => $tier,
182 label => $label,
183 selected_attr => ($tier eq $current_tier) ? ' selected' : '',
184 };
185 }
186 $db->db_disconnect($dbh);
187 return @out if scalar @out;
188 }
189 $db->db_disconnect($dbh) if $dbh;
190
191 # Fallback for un-migrated databases.
192 foreach my $r (
193 { v=>'free', l=>'Free &mdash; 10 models, 2 platforms' },
194 { v=>'starter', l=>'Starter &mdash; $9/mo, unlimited models' },
195 { v=>'pro', l=>'Pro &mdash; $29/mo, storefront + optimization' },
196 { v=>'studio', l=>'Studio &mdash; $99/mo, teams + multi-store' },
197 ) {
198 push @out, {
199 value => $r->{v},
200 label => $r->{l},
201 selected_attr => ($r->{v} eq $current_tier) ? ' selected' : '',
202 };
203 }
204 return @out;
205}
206
207sub _signup_price_label {
208 my ($cents) = @_;
209 $cents = 0 unless defined $cents;
210 return 'free forever' if $cents == 0;
211 if ($cents % 100 == 0) {
212 return '$' . int($cents / 100) . '/mo';
213 }
214 return '$' . sprintf('%.2f', $cents / 100) . '/mo';
215}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help