added on local at 2026-07-01 21:47:18
| 1 | #!/usr/bin/perl | |
| 2 | #====================================================================== | |
| 3 | # RePricer -- active sessions / device management. | |
| 4 | # | |
| 5 | # GET /sessions.cgi list active sessions for this user | |
| 6 | # POST act=revoke id=UUID revoke one session by id | |
| 7 | # POST act=revoke_all revoke every session EXCEPT the current one | |
| 8 | #====================================================================== | |
| 9 | use strict; | |
| 10 | use warnings; | |
| 11 | use lib '/var/www/vhosts/3dshawn.com/repricer.3dshawn.com'; | |
| 12 | use CGI; | |
| 13 | use MODS::DBConnect; | |
| 14 | use MODS::Login; | |
| 15 | use MODS::RePricer::Config; | |
| 16 | use MODS::RePricer::Wrapper; | |
| 17 | ||
| 18 | $|=1; | |
| 19 | my $q = CGI->new; | |
| 20 | my $form = $q->Vars; | |
| 21 | my $auth = MODS::Login->new; | |
| 22 | my $wrap = MODS::RePricer::Wrapper->new; | |
| 23 | my $db = MODS::DBConnect->new; | |
| 24 | my $cfg = MODS::RePricer::Config->new; | |
| 25 | my $DB = $cfg->settings('database_name'); | |
| 26 | ||
| 27 | my $userinfo = $auth->login_verify(); | |
| 28 | unless ($userinfo) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; } | |
| 29 | my $uid = $userinfo->{user_id}; $uid =~ s/[^0-9]//g; | |
| 30 | my $sid = $userinfo->{session_id}; $sid =~ s/[^a-zA-Z0-9\-]//g; | |
| 31 | ||
| 32 | sub _h { my $s = shift // ''; $s =~ s/&/&/g; $s =~ s/</</g; $s =~ s/>/>/g; $s =~ s/"/"/g; $s } | |
| 33 | ||
| 34 | my $dbh = $db->db_connect(); | |
| 35 | my $flash = ''; | |
| 36 | if (($ENV{REQUEST_METHOD} || '') eq 'POST') { | |
| 37 | my $act = $form->{act} || ''; | |
| 38 | if ($act eq 'revoke') { | |
| 39 | my $target = $form->{id} || ''; $target =~ s/[^a-zA-Z0-9\-]//g; | |
| 40 | if ($target && $target ne $sid) { | |
| 41 | $db->db_readwrite($dbh, qq~ | |
| 42 | UPDATE `${DB}`.user_sessions SET revoked_at=NOW() | |
| 43 | WHERE id='$target' AND user_id='$uid' | |
| 44 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 45 | $flash = 'Session revoked.'; | |
| 46 | } | |
| 47 | } elsif ($act eq 'revoke_all') { | |
| 48 | $db->db_readwrite($dbh, qq~ | |
| 49 | UPDATE `${DB}`.user_sessions SET revoked_at=NOW() | |
| 50 | WHERE user_id='$uid' AND id<>'$sid' AND revoked_at IS NULL | |
| 51 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 52 | $flash = 'All other sessions revoked.'; | |
| 53 | } | |
| 54 | } | |
| 55 | ||
| 56 | my $rows = $db->db_readwrite_multiple($dbh, qq~ | |
| 57 | SELECT id, ip_address, user_agent, issued_at, expires_at, | |
| 58 | TIMESTAMPDIFF(MINUTE, issued_at, NOW()) AS mins_old | |
| 59 | FROM `${DB}`.user_sessions | |
| 60 | WHERE user_id='$uid' AND revoked_at IS NULL AND expires_at > NOW() | |
| 61 | ORDER BY issued_at DESC | |
| 62 | ~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 63 | $db->db_disconnect($dbh); | |
| 64 | ||
| 65 | my $body = '<div style="max-width:800px;margin:0 auto;padding:24px 28px;color:#e6ecf6">'; | |
| 66 | $body .= '<h1 style="font-size:26px;color:#fff;margin:0 0 6px">Active sessions</h1>'; | |
| 67 | $body .= '<p style="color:#7e92b6;margin:0 0 18px">Every browser / device currently signed in. If you see something you don't recognize, revoke it now and change your password.</p>'; | |
| 68 | ||
| 69 | if ($flash) { | |
| 70 | $body .= '<div style="background:#10b98122;border-left:3px solid #10b981;color:#10b981;padding:10px 14px;border-radius:6px;margin-bottom:14px">' . _h($flash) . '</div>'; | |
| 71 | } | |
| 72 | ||
| 73 | $body .= '<section style="background:#0b1226;border:1px solid #1f2a4a;border-radius:12px;overflow:hidden">'; | |
| 74 | $body .= '<table style="width:100%;font-size:13px;border-collapse:collapse">'; | |
| 75 | $body .= '<thead><tr style="background:#0a0f1f;color:#7e92b6;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:1px"><th style="padding:10px 14px">IP</th><th style="padding:10px 14px">User agent</th><th style="padding:10px 14px">Signed in</th><th style="padding:10px 14px">Expires</th><th style="padding:10px 14px"></th></tr></thead><tbody>'; | |
| 76 | ||
| 77 | foreach my $r (@$rows) { | |
| 78 | my $is_current = $r->{id} eq $sid ? 1 : 0; | |
| 79 | my $ip = $r->{ip_address} ? unpack('H*', $r->{ip_address}) : '?'; | |
| 80 | my $ua = substr($r->{user_agent} // '', 0, 60); | |
| 81 | $ua .= '...' if length($r->{user_agent} // '') > 60; | |
| 82 | my $age = $r->{mins_old} // 0; | |
| 83 | my $age_str = $age < 60 ? "$age min ago" | |
| 84 | : $age < 1440 ? int($age/60) . " hr ago" | |
| 85 | : int($age/1440) . " days ago"; | |
| 86 | my $cur_pill = $is_current ? ' <span style="background:#064e3b;color:#a7f3d0;padding:2px 8px;border-radius:999px;font-size:10px;font-weight:700;letter-spacing:1px">CURRENT</span>' : ''; | |
| 87 | $body .= '<tr style="border-top:1px solid #1f2a4a">'; | |
| 88 | $body .= '<td style="padding:10px 14px;font-family:ui-monospace,Menlo,monospace;color:#cbd5e1;font-size:11px">' . _h($ip) . '</td>'; | |
| 89 | $body .= '<td style="padding:10px 14px;color:#e6ecf6">' . _h($ua) . $cur_pill . '</td>'; | |
| 90 | $body .= '<td style="padding:10px 14px;color:#7e92b6">' . _h($age_str) . '</td>'; | |
| 91 | $body .= '<td style="padding:10px 14px;color:#7e92b6">' . _h($r->{expires_at}) . '</td>'; | |
| 92 | if ($is_current) { | |
| 93 | $body .= '<td style="padding:10px 14px;color:#7e92b6;font-size:11px">--</td>'; | |
| 94 | } else { | |
| 95 | $body .= '<td style="padding:10px 14px"><form method="POST" style="display:inline" onsubmit="return confirm(\'Revoke this session?\')"><input type="hidden" name="act" value="revoke"><input type="hidden" name="id" value="' . _h($r->{id}) . '"><button type="submit" style="background:transparent;color:#fca5a5;border:0;cursor:pointer;font-size:12px">Revoke</button></form></td>'; | |
| 96 | } | |
| 97 | $body .= '</tr>'; | |
| 98 | } | |
| 99 | $body .= '</tbody></table></section>'; | |
| 100 | ||
| 101 | $body .= '<form method="POST" style="margin-top:14px" onsubmit="return confirm(\'Revoke every session except your current one?\')">'; | |
| 102 | $body .= ' <input type="hidden" name="act" value="revoke_all">'; | |
| 103 | $body .= ' <button type="submit" style="background:#1f2a4a;color:#fca5a5;border:1px solid #ef4444;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:13px">Revoke all other sessions</button>'; | |
| 104 | $body .= '</form>'; | |
| 105 | ||
| 106 | $body .= '<div style="margin-top:24px;color:#7e92b6;font-size:12px">Lost a device? Revoke its session here. Spotted something suspicious? Revoke + change your <a href="/profile.cgi" style="color:#14b8a6">password</a> + enable <a href="/two_factor_setup.cgi" style="color:#14b8a6">2FA</a>.</div>'; | |
| 107 | $body .= '</div>'; | |
| 108 | ||
| 109 | print "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"; | |
| 110 | $wrap->render({ userinfo => $userinfo, title => 'Active sessions', body => $body }); | |
| 111 | exit; |