added on local at 2026-07-02 16:53:21
| 1 | #!/usr/bin/perl | |
| 2 | #====================================================================== | |
| 3 | # AffSoft - My Account: close (self-cancel) | |
| 4 | # | |
| 5 | # Self-service: authenticated user can cancel their own account. | |
| 6 | # Sets account_state='cancelled', cancelled_at=NOW(), kills sessions. | |
| 7 | # Data is preserved. Login is blocked until an admin reinstates. | |
| 8 | # | |
| 9 | # For sites with paid subscriptions, this is the "cancel-at-period-end" | |
| 10 | # equivalent: the row stays in Stripe (subscription lifecycle is | |
| 11 | # managed elsewhere via stripe_webhook.cgi). This CGI only flips the | |
| 12 | # app-side account_state so login is denied cleanly. | |
| 13 | # | |
| 14 | # Auto-generated 2026-07-02 from phase89_my_account_close.pl.tmpl | |
| 15 | #====================================================================== | |
| 16 | use strict; use warnings; | |
| 17 | use lib '/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com'; | |
| 18 | use CGI; | |
| 19 | use MODS::Template; | |
| 20 | use MODS::DBConnect; | |
| 21 | use MODS::Login; | |
| 22 | use MODS::AffSoft::Config; | |
| 23 | use MODS::AffSoft::Wrapper; | |
| 24 | ||
| 25 | my $q = CGI->new; | |
| 26 | my $form = $q->Vars; | |
| 27 | my $auth = MODS::Login->new; | |
| 28 | my $wrap = MODS::AffSoft::Wrapper->new; | |
| 29 | my $db = MODS::DBConnect->new; | |
| 30 | my $cfg = MODS::AffSoft::Config->new; | |
| 31 | my $DB = $cfg->settings('database_name'); | |
| 32 | ||
| 33 | my $userinfo = $auth->login_verify(); | |
| 34 | if (!$userinfo) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; } | |
| 35 | ||
| 36 | # ---- Handle POST: confirm self-cancel ---------------------------------- | |
| 37 | if (($ENV{REQUEST_METHOD} || '') eq 'POST' && ($form->{action} || '') eq 'self_cancel') { | |
| 38 | # Require explicit typed confirmation to prevent accidental cancels. | |
| 39 | my $typed = $form->{confirm_text} || ''; | |
| 40 | $typed =~ s/^\s+|\s+$//g; | |
| 41 | if (lc($typed) ne 'cancel my account') { | |
| 42 | _render_page("Please type <strong>cancel my account</strong> exactly to confirm."); | |
| 43 | exit; | |
| 44 | } | |
| 45 | my $uid = $userinfo->{_admin_user_id} || $userinfo->{user_id}; | |
| 46 | $uid =~ s/[^0-9]//g; | |
| 47 | if ($uid) { | |
| 48 | my $dbh = $db->db_connect(); | |
| 49 | if ($dbh) { | |
| 50 | # Flip account_state and stamp cancelled_at. | |
| 51 | $db->db_readwrite($dbh, qq~ | |
| 52 | UPDATE ${DB}.users | |
| 53 | SET account_state = 'cancelled', | |
| 54 | cancelled_at = NOW() | |
| 55 | WHERE id = '$uid' | |
| 56 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 57 | # Kill all active sessions for this user. | |
| 58 | $db->db_readwrite($dbh, qq~ | |
| 59 | DELETE FROM ${DB}.user_sessions WHERE user_id = '$uid' | |
| 60 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 61 | $db->db_disconnect($dbh); | |
| 62 | } | |
| 63 | } | |
| 64 | # Redirect to login with a marker so we can show a "your account was cancelled" message. | |
| 65 | print "Status: 302 Found\nLocation: /login.cgi?cancelled=1\n\n"; | |
| 66 | exit; | |
| 67 | } | |
| 68 | ||
| 69 | # ---- GET: show the confirm form --------------------------------------- | |
| 70 | _render_page(''); | |
| 71 | exit; | |
| 72 | ||
| 73 | ||
| 74 | sub _render_page { | |
| 75 | my ($error) = @_; | |
| 76 | my $email = _esc($userinfo->{email} // ''); | |
| 77 | my $err_html = $error ? qq~<div class="myc-err">$error</div>~ : ''; | |
| 78 | my $accent = '#a855f7'; | |
| 79 | my $body = qq~ | |
| 80 | <style> | |
| 81 | .myc-shell { padding: 40px 40px 80px; max-width: 720px; margin: 0 auto; } | |
| 82 | .myc-title { color: #fff; font-size: 28px; font-weight: 800; letter-spacing: -0.01em; margin-bottom: 6px; } | |
| 83 | .myc-sub { color: #9aa4b7; font-size: 15px; margin-bottom: 26px; } | |
| 84 | .myc-card { background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.06); border-radius: 14px; padding: 26px; } | |
| 85 | .myc-warn { padding: 14px 16px; margin-bottom: 20px; background: rgba(245,158,11,0.10); border: 1px solid rgba(245,158,11,0.35); border-radius: 10px; color: #fbbf24; font-size: 14px; } | |
| 86 | .myc-what { margin: 0 0 22px; padding-left: 20px; color: #cfd6e0; font-size: 14px; line-height: 1.7; } | |
| 87 | .myc-what li{ margin: 6px 0; } | |
| 88 | .myc-what li.keep::marker { color: #6ee7b7; } | |
| 89 | .myc-what li.stop::marker { color: #fca5a5; } | |
| 90 | .myc-input { width: 100%; box-sizing: border-box; background: #161c25; border: 1px solid rgba(255,255,255,0.08); color: #e8edf2; padding: 12px 14px; border-radius: 10px; font-size: 14px; margin-bottom: 14px; } | |
| 91 | .myc-actions{ display: flex; gap: 10px; align-items: center; } | |
| 92 | .myc-btn { display: inline-flex; align-items: center; gap: 6px; padding: 12px 22px; border-radius: 10px; font-size: 14px; font-weight: 600; text-decoration: none; border: none; cursor: pointer; } | |
| 93 | .myc-btn.cancel { background: rgba(239,68,68,0.14); color: #fca5a5; border: 1px solid rgba(239,68,68,0.42); } | |
| 94 | .myc-btn.cancel:hover { background: rgba(239,68,68,0.20); } | |
| 95 | .myc-btn.ghost { background: rgba(255,255,255,0.05); color: #cfd6e0; border: 1px solid rgba(255,255,255,0.08); } | |
| 96 | .myc-err { padding: 12px 14px; margin-bottom: 16px; background: rgba(239,68,68,0.12); border: 1px solid rgba(239,68,68,0.35); border-radius: 10px; color: #fca5a5; font-size: 14px; } | |
| 97 | </style> | |
| 98 | <div class="myc-shell"> | |
| 99 | <div class="myc-title">Close my account</div> | |
| 100 | <div class="myc-sub">Signed in as <strong>$email</strong></div> | |
| 101 | ||
| 102 | <div class="myc-card"> | |
| 103 | <div class="myc-warn"> | |
| 104 | Your data is preserved. If you change your mind, contact support and an admin | |
| 105 | can reinstate the account. | |
| 106 | </div> | |
| 107 | ||
| 108 | <ul class="myc-what"> | |
| 109 | <li class="stop">Your login will be disabled immediately.</li> | |
| 110 | <li class="stop">Any active sessions will be signed out.</li> | |
| 111 | <li class="keep">Your account data (settings, history) stays on file.</li> | |
| 112 | <li class="keep">Reinstatement is one click for an admin.</li> | |
| 113 | </ul> | |
| 114 | ||
| 115 | $err_html | |
| 116 | ||
| 117 | <form method="POST" onsubmit="return confirm('Really close this account?')"> | |
| 118 | <input type="hidden" name="action" value="self_cancel"/> | |
| 119 | <label for="confirm_text" style="display:block;color:#cfd6e0;font-size:13px;margin-bottom:8px"> | |
| 120 | Type <strong>cancel my account</strong> to confirm: | |
| 121 | </label> | |
| 122 | <input class="myc-input" id="confirm_text" name="confirm_text" autocomplete="off" placeholder="cancel my account"/> | |
| 123 | <div class="myc-actions"> | |
| 124 | <button class="myc-btn cancel" type="submit">Close my account</button> | |
| 125 | <a class="myc-btn ghost" href="/dashboard.cgi">Never mind</a> | |
| 126 | </div> | |
| 127 | </form> | |
| 128 | </div> | |
| 129 | </div> | |
| 130 | ~; | |
| 131 | print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n"; | |
| 132 | $wrap->render({ | |
| 133 | userinfo => $userinfo, | |
| 134 | title => 'Close my account', | |
| 135 | body => $body, | |
| 136 | }); | |
| 137 | } | |
| 138 | ||
| 139 | sub _esc { | |
| 140 | my $s = defined $_[0] ? $_[0] : ''; | |
| 141 | $s =~ s/&/&/g; $s =~ s/</</g; $s =~ s/>/>/g; | |
| 142 | $s =~ s/"/"/g; $s =~ s/'/'/g; | |
| 143 | return $s; | |
| 144 | } |