Diff -- /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/login.cgi
Diff

/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/login.cgi

added on local at 2026-07-01 12:34:16

Added
+456
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 84d43b581ffe
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##################################################################################################################################
4# ______ ____ ____ ______ ______ ____ ___ __ ___ ______ _ __ ____ ____ __ __ _ __ _____ ____
5# / ____// __ \ / __ \ / ____/ / ____// __ \ / | / |/ // ____/| | / // __ \ / __ \ / //_/ | | / /|__ / / __ \
6# / / / / / // / / // __/ / /_ / /_/ // /| | / /|_/ // __/ | | /| / // / / // /_/ // ,< | | / / /_ < / / / /
7# / /___ / /_/ // /_/ // /___ / __/ / _, _// ___ | / / / // /___ | |/ |/ // /_/ // _, _// /| | | |/ / ___/ /_ / /_/ /
8# \____/ \____//_____//_____/ /_/ /_/ |_|/_/ |_|/_/ /_//_____/ |__/|__/ \____//_/ |_|/_/ |_| |___/ /____/(_)\____/
9#
10# - Written by: Shawn Pickering
11# - shawn@shawnpickering.com
12##################################################################################################################################
13use strict;
14use lib '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com';
15
16
17#############################################################
18## Variables
19#############################################################
20
21
22
23############################################################
24# Modules
25############################################################
26use CGI; my $query = new CGI; my $form = $query->Vars;
27use MODS::Login; my $login = new MODS::Login;
28use MODS::PTMatrix::Urls; my $getlist = new MODS::PTMatrix::Urls; my $url = $getlist->urls();
29use MODS::Template; my $tfile = new MODS::Template;
30use MODS::PTMatrix::Wrapper; my $load = new MODS::PTMatrix::Wrapper;
31use MODS::DBConnect; my $db = new MODS::DBConnect;
32use MODS::PTMatrix::Config; my $config = new MODS::PTMatrix::Config; my $database_name = $config->settings('database_name');
33use MODS::PTMatrix::PM;
34
35
36
37#############################################################
38## Get form data
39#############################################################
40#Snapshot raw values BEFORE quotemeta - login_exec / signup_company / update_password /
41#accept_invite all need the raw email + password to mint or verify the hash
42my %rawform = %$form;
43
44foreach my $line(keys %$form){
45 $form->{$line} =~ s/[^!-~\s]//g;
46 $form->{$line} = quotemeta("$form->{$line}");
47}
48
49
50
51############################################################
52# Program Controls
53############################################################
54$|=1;
55my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'login';
56my $is_post = (($ENV{REQUEST_METHOD} || '') eq 'POST') ? 1 : 0;
57
58if($entry eq 'signup'){
59 my $userinfo = $login->login_verify();
60 if($userinfo){print qq~Status: 302 Found\nLocation: $url->{dashboard}\n\n~; exit;}
61 if($is_post){&try_signup;}
62 else {&signup_form;}
63}elsif($entry eq 'forgot_password'){
64 print qq~Content-type:text/html; Cache-Control:no-cache;\n\n~;
65 if($is_post && $form->{email}){&send_reset;}
66 else {&forgot_form;}
67}elsif($entry eq 'reset_password'){
68 my $token = $form->{token} || '';
69 $token =~ s/[^A-Za-z0-9]//g;
70 if($is_post){&do_reset($token);}
71 else {&reset_form($token);}
72}elsif($entry eq 'accept_invite'){
73 my $token = $form->{token} || '';
74 $token =~ s/[^A-Za-z0-9_\-]//g;
75 if($is_post){&do_accept($token);}
76 else {&invite_form($token);}
77}else{
78 #Default = login
79 my $userinfo = $login->login_verify();
80 if($userinfo){print qq~Status: 302 Found\nLocation: $url->{dashboard}\n\n~; exit;}
81 if($is_post){&try_login;}
82 else {&login_form;}
83}
84
85
86############################################################
87# Subroutines
88############################################################
89sub h{
90 my $s = shift // '';
91 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
92 $s =~ s/"/&quot;/g; $s =~ s/'/&#39;/g;
93 return $s;
94}
95
96sub maintenance_overlay{
97 #Reused between login_form + signup_form to draw the maintenance modal
98 my $mmsg = $config->settings('maintenance_message') || '<p>The system is in maintenance mode.</p>';
99 return 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>};
100}
101
102#============================================================
103# login entry
104#============================================================
105sub try_login{
106 my $email = $rawform{email} || '';
107 my $pass = $rawform{password} || '';
108
109 #SSO enforcement - if this email belongs to a company with an active SSO config that
110 #enforces SSO (and doesn't allow password fallback), bounce them to /sso_login.cgi
111 if($email){
112 my $dbh = $db->db_connect();
113 if($dbh){
114 my $se = $email; $se =~ s/[\\']/\\$&/g;
115 my $sql = qq~SELECT s.id AS sso_id, s.enforce, s.allow_password_fb, u.is_super_admin FROM `${database_name}`.users u JOIN `${database_name}`.company_sso_configs s ON s.company_id=u.company_id AND s.status='active' WHERE u.email='$se' AND u.account_status='active' ORDER BY s.enforce DESC LIMIT 1~;
116 my $r = $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
117 $db->db_disconnect($dbh);
118 if($r && $r->{sso_id} && $r->{enforce} && !$r->{allow_password_fb} && !$r->{is_super_admin}){
119 require URI::Escape;
120 my $qe = URI::Escape::uri_escape($email);
121 print qq~Status: 302 Found\nLocation: $url->{sso_login}&email=$qe\n\n~;
122 exit;
123 }
124 }
125 }
126
127 my ($u_row, $token, $needs_2fa) = $login->login_exec($email, $pass);
128
129 if($u_row && $token){
130 print $login->set_cookie_header($token);
131 my $dest = $needs_2fa ? $url->{twofa} : $url->{dashboard};
132 print qq~Status: 302 Found\nLocation: $dest\n\n~;
133 exit;
134 }
135
136 &login_form('Email or password is incorrect.', $email);
137}
138
139sub login_form{
140 my($error_msg, $email_val) = @_;
141 $error_msg ||= '';
142 $email_val ||= '';
143
144 #Friendly flash messages routed in via query string from reset / signup flows
145 my $flash_msg = '';
146 if(defined $form->{reset} && $form->{reset} eq 'ok'){$flash_msg = 'Password updated. Sign in with your new password.';}
147 if(defined $form->{signup} && $form->{signup} eq 'ok'){$flash_msg = 'Workspace created. Sign in to get started.';}
148
149 print qq~Content-type:text/html; Cache-Control:no-cache;\n\n~;
150
151 my $tvars;
152 $tvars->{error_msg} = $error_msg;
153 $tvars->{has_error} = length($error_msg) ? 1 : 0;
154 $tvars->{flash_msg} = $flash_msg;
155 $tvars->{has_flash} = length($flash_msg) ? 1 : 0;
156 $tvars->{email} = &h($email_val);
157
158 my $body = join('', $tfile->template('tf_login.html', $tvars, undef));
159
160 if($config->settings('maintenance_locked')){
161 $body = &maintenance_overlay . $body;
162 }
163
164 $load->render({
165 userinfo => undef,
166 title => 'Sign in',
167 body => $body,
168 });
169}
170
171#============================================================
172# signup entry
173#============================================================
174sub try_signup{
175 my $company = $rawform{company_name} || '';
176 my $email = $rawform{email} || '';
177 my $pass = $rawform{password} || '';
178 my $name = $rawform{display_name} || '';
179 my $accept = $rawform{accept_terms} || '';
180
181 my $form_vals = {
182 company_name => &h($company),
183 display_name => &h($name),
184 email => &h($email),
185 plan => $form->{plan},
186 };
187
188 my $error_msg = '';
189 if(!$company || !$email || !$pass || !$name){
190 $error_msg = 'All fields are required.';
191 }elsif(length($pass) < 8){
192 $error_msg = 'Password must be at least 8 characters.';
193 }elsif(!$accept){
194 $error_msg = 'Please accept the Terms of Service to continue.';
195 }
196
197 if($error_msg){
198 &signup_form($error_msg, $form_vals);
199 return;
200 }
201
202 my $chosen_plan = $form->{plan} || '';
203 $chosen_plan = '' unless $chosen_plan =~ /^(free|starter|pro|business)$/;
204 my ($user_id, $company_id) = $login->signup_company($company, $email, $pass, $name, $chosen_plan);
205
206 if(!$user_id){
207 &signup_form('That email is already in use. Try signing in instead.', $form_vals);
208 return;
209 }
210
211 #Seed sample data + link the anon-tracker session to the new user
212 eval{
213 require MODS::PTMatrix::Onboarding;
214 my $dbh = $db->db_connect();
215 if($dbh){
216 MODS::PTMatrix::Onboarding->new->seed_sample_data($db, $dbh, $database_name, $company_id, $user_id);
217
218 my $anon_tok = $query->cookie('tf_a') || '';
219 $anon_tok =~ s/[^a-f0-9]//g;
220 if(length($anon_tok) == 40){
221 my $uid_i = $user_id + 0;
222 my $sql = qq~UPDATE `${database_name}`.anon_sessions SET end_reason='signed_up', signed_up_user_id='$uid_i', last_seen_at=NOW() WHERE session_token='$anon_tok' AND end_reason='active'~;
223 $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
224 }
225
226 $db->db_disconnect($dbh);
227 }
228 };
229
230 my ($u_row, $token) = $login->login_exec($email, $pass);
231 if($u_row && $token){
232 print $login->set_cookie_header($token);
233 print qq~Status: 302 Found\nLocation: $url->{dashboard}&welcome=1\n\n~;
234 exit;
235 }
236
237 print qq~Status: 302 Found\nLocation: $url->{login}&signup=ok\n\n~;
238 exit;
239}
240
241sub signup_form{
242 my($error_msg, $form_vals) = @_;
243 $error_msg ||= '';
244 $form_vals ||= { company_name => '', display_name => '', email => '', plan => $form->{plan} || '' };
245
246 my $is_free = (($form->{plan} || '') eq 'free') ? 1 : 0;
247
248 print qq~Content-type:text/html; Cache-Control:no-cache;\n\n~;
249
250 my $tvars;
251 $tvars->{error_msg} = $error_msg;
252 $tvars->{has_error} = length($error_msg) ? 1 : 0;
253 $tvars->{is_free} = $is_free;
254 $tvars->{headline} = $is_free ? 'Create your free workspace' : 'Start your free trial';
255 $tvars->{subhead} = $is_free
256 ? 'Free forever for up to 3 users. No credit card. Upgrade anytime.'
257 : 'Try every feature for 14 days. No credit card. Cancel anytime.';
258 $tvars->{company_name} = $form_vals->{company_name};
259 $tvars->{display_name} = $form_vals->{display_name};
260 $tvars->{email} = $form_vals->{email};
261 $tvars->{plan} = $form_vals->{plan};
262
263 my $body = join('', $tfile->template('tf_signup.html', $tvars, undef));
264
265 if($config->settings('maintenance_locked')){
266 $body = &maintenance_overlay . $body;
267 }
268
269 $load->render({
270 userinfo => undef,
271 title => $is_free ? 'Get started free' : 'Start your free trial',
272 body => $body,
273 });
274}
275
276#============================================================
277# forgot_password entry
278#============================================================
279sub send_reset{
280 my $dbh = $db->db_connect();
281
282 #Look up an active user by the submitted email
283 my $email = MODS::PTMatrix::PM::sql_quote($rawform{email});
284 my $sql = qq~SELECT id FROM `${database_name}`.users WHERE email='$email' AND account_status='active' LIMIT 1~;
285 my $u = $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
286
287 #Mint a 48-char token + insert the reset row (expires in 1 hour) if the user exists
288 my $reset_url = '';
289 if($u && $u->{id}){
290 my @c = ('a'..'z','A'..'Z','0'..'9');
291 my $token = '';
292 $token .= $c[int(rand 62)] for 1..48;
293
294 my $sql_ins = qq~INSERT INTO `${database_name}`.password_resets (token, user_id, expires_at, created_at) VALUES ('$token', '$u->{id}', DATE_ADD(NOW(), INTERVAL 1 HOUR), NOW())~;
295 $db->db_readwrite($dbh, $sql_ins, $ENV{SCRIPT_NAME}, __LINE__);
296
297 $reset_url = 'https://ptmatrix.3dshawn.com/reset_password.cgi?token=' . $token;
298 }
299
300 $db->db_disconnect($dbh);
301
302 #Always re-render with sent=1 (don't leak whether the email existed)
303 &forgot_form(1, $reset_url);
304}
305
306sub forgot_form{
307 my($sent, $reset_url) = @_;
308 $sent ||= 0;
309 $reset_url ||= '';
310
311 my $tvars;
312 $tvars->{sent} = $sent;
313 $tvars->{reset_url} = $reset_url;
314 $tvars->{has_link} = length($reset_url) ? 1 : 0;
315
316 my $body = join('', $tfile->template('tf_forgot_password.html', $tvars, undef));
317
318 $load->render({
319 userinfo => undef,
320 title => 'Reset your password',
321 body => $body,
322 });
323}
324
325#============================================================
326# reset_password entry
327#============================================================
328sub do_reset{
329 my($token) = @_;
330
331 if(!$token){
332 &reset_form($token, 'Missing reset token.');
333 return;
334 }
335
336 my $dbh = $db->db_connect();
337
338 my $sql = qq~SELECT user_id FROM `${database_name}`.password_resets WHERE token='$token' AND expires_at > NOW() AND used_at IS NULL LIMIT 1~;
339 my $r = $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
340
341 if(!$r || !$r->{user_id}){
342 $db->db_disconnect($dbh);
343 &reset_form($token, 'This reset link is invalid or expired.');
344 return;
345 }
346
347 my $user_id = $r->{user_id};
348 my $pw = $rawform{password} || '';
349
350 if(length($pw) < 8){
351 $db->db_disconnect($dbh);
352 &reset_form($token, 'Password must be at least 8 characters.');
353 return;
354 }
355
356 my $ok = $login->update_password($user_id, $pw);
357 if(!$ok){
358 $db->db_disconnect($dbh);
359 &reset_form($token, 'Could not update password.');
360 return;
361 }
362
363 my $sql_used = qq~UPDATE `${database_name}`.password_resets SET used_at=NOW() WHERE token='$token'~;
364 $db->db_readwrite($dbh, $sql_used, $ENV{SCRIPT_NAME}, __LINE__);
365 $db->db_disconnect($dbh);
366
367 print qq~Status: 302 Found\nLocation: $url->{login}&reset=ok\n\n~;
368 exit;
369}
370
371sub reset_form{
372 my($token, $error_msg) = @_;
373 $error_msg ||= '';
374 if(!$error_msg && !$token){$error_msg = 'Missing reset token.';}
375
376 print qq~Content-type:text/html; Cache-Control:no-cache;\n\n~;
377
378 my $tvars;
379 $tvars->{error_msg} = $error_msg;
380 $tvars->{has_error} = length($error_msg) ? 1 : 0;
381 $tvars->{token} = $token;
382
383 my $body = join('', $tfile->template('tf_reset_password.html', $tvars, undef));
384
385 $load->render({
386 userinfo => undef,
387 title => 'Reset password',
388 body => $body,
389 });
390}
391
392#============================================================
393# accept_invite entry
394#============================================================
395sub do_accept{
396 my($token) = @_;
397
398 if(!$token){
399 &invite_form($token, 'Missing invite token.');
400 return;
401 }
402
403 my $pass = $rawform{password} || '';
404 if(length($pass) < 8){
405 &invite_form($token, 'Password must be at least 8 characters.');
406 return;
407 }
408
409 my ($u_row, $sess) = $login->accept_invite($token, $pass);
410
411 if($u_row && $sess){
412 print $login->set_cookie_header($sess);
413 print qq~Status: 302 Found\nLocation: $url->{dashboard}&welcome=1\n\n~;
414 exit;
415 }
416
417 &invite_form($token, 'Sorry, could not complete the invite.');
418}
419
420sub invite_form{
421 my($token, $error_msg) = @_;
422 $error_msg ||= '';
423
424 my $email = '';
425 my $cname = '';
426 if($token && !$error_msg){
427 my $dbh = $db->db_connect();
428 my $sql = qq~SELECT u.email, u.display_name, c.name AS company_name FROM `${database_name}`.users u JOIN `${database_name}`.companies c ON c.id=u.company_id WHERE u.invite_token='$token' AND u.invite_expires_at > NOW() AND u.account_status='pending_verification' LIMIT 1~;
429 my $r = $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
430 $db->db_disconnect($dbh);
431 if($r && $r->{email}){
432 $email = $r->{email};
433 $cname = $r->{company_name};
434 }else{
435 $error_msg = 'Invite link is invalid or expired.';
436 }
437 }elsif(!$token){
438 $error_msg = 'Missing invite token.';
439 }
440
441 print qq~Content-type:text/html; Cache-Control:no-cache;\n\n~;
442
443 my $tvars;
444 $tvars->{error_msg} = $error_msg;
445 $tvars->{has_error} = length($error_msg) ? 1 : 0;
446 $tvars->{email} = $email;
447 $tvars->{company_name} = $cname;
448
449 my $body = join('', $tfile->template('tf_accept_invite.html', $tvars, undef));
450
451 $load->render({
452 userinfo => undef,
453 title => 'Accept invitation',
454 body => $body,
455 });
456}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help