Diff -- /var/www/vhosts/3dshawn.com/crm.3dshawn.com/MODS/ContactForge/Admin.pm

O Operator
Diff

/var/www/vhosts/3dshawn.com/crm.3dshawn.com/MODS/ContactForge/Admin.pm

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

Added
+0
lines
Removed
-0
lines
Context
123
unchanged
Blobs
from 464327fa555c
to 464327fa555c
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
11package MODS::ContactForge::Admin;
22#======================================================================
33# ContactForge -- Admin / company-staff helpers.
44#
55# This module is the single chokepoint that enforces the line between
66# regular reps and company staff. Every admin-only endpoint must
77# call require_admin() near the top; the sidebar entry in Wrapper.pm
88# is gated on is_admin_userinfo().
99#
1010# Impersonation:
1111# start_impersonation($db, $dbh, $DB, $admin_session_id, $admin_user_id, $target_user_id, $mode)
1212# stop_impersonation ($db, $dbh, $DB, $admin_session_id)
1313#
1414# The state lives in the admin_impersonation table -- the admin's
1515# existing user_sessions row IS the auth principal, so an admin who
1616# logs out automatically loses impersonation (CASCADE).
1717#======================================================================
1818use strict;
1919use warnings;
2020
2121sub new {
2222 my ($class) = @_;
2323 return bless({}, $class);
2424}
2525
2626# Is the currently authenticated principal a company admin? Returns 1
2727# whether they're acting as themselves or impersonating someone else.
2828# During impersonation the userinfo row is the target user's, but
2929# _admin_is_admin is preserved as the admin's role flag.
3030sub is_admin_userinfo {
3131 my ($self, $userinfo) = @_;
3232 return 0 unless $userinfo;
3333 return 1 if $userinfo->{_admin_is_admin}; # impersonating
3434 return 1 if $userinfo->{is_admin}; # acting as self
3535 return 0;
3636}
3737
3838# Gate an endpoint. Call before doing any admin-only work. Emits a 302
3939# to /dashboard.cgi (NOT /login) so a logged-in non-admin who finds the
4040# URL just lands on their own dashboard instead of being told the page
4141# exists.
4242sub require_admin {
4343 my ($self, $userinfo) = @_;
4444 return if $self->is_admin_userinfo($userinfo);
4545 print "Status: 302 Found\nLocation: /dashboard.cgi\n\n";
4646 exit;
4747}
4848
4949# Real admin id even while impersonating. Use this for any audit-log
5050# write or any "who pressed the button" attribution. Falls back to the
5151# current user_id when not impersonating (admin acting as themselves).
5252sub admin_user_id {
5353 my ($self, $userinfo) = @_;
5454 return 0 unless $userinfo;
5555 return $userinfo->{_admin_user_id} || $userinfo->{user_id} || 0;
5656}
5757
5858# Admin session id (the auth principal's session, NOT the impersonation
5959# target's). Login.pm puts the session id on userinfo regardless of
6060# impersonation state.
6161sub admin_session_id {
6262 my ($self, $userinfo) = @_;
6363 return '' unless $userinfo;
6464 my $s = $userinfo->{session_id} || '';
6565 $s =~ s/[^a-zA-Z0-9\-]//g;
6666 return $s;
6767}
6868
6969# Does the admin_impersonation table exist on this DB? Many code paths
7070# call this before SELECTing to avoid a 500 when the migration hasn't
7171# been run yet (see feedback_guard_optional_tables memory).
7272sub _has_table {
7373 my ($self, $db, $dbh, $DB) = @_;
7474 return $self->{_has_table} if exists $self->{_has_table};
7575 my $r = $db->db_readwrite($dbh, qq~
7676 SELECT COUNT(*) AS n FROM information_schema.tables
7777 WHERE table_schema='$DB' AND table_name='admin_impersonation'
7878 ~, $ENV{SCRIPT_NAME}, __LINE__);
7979 $self->{_has_table} = ($r && $r->{n}) ? 1 : 0;
8080 return $self->{_has_table};
8181}
8282
8383# Start acting as another user. Caller is responsible for verifying
8484# admin status BEFORE invoking (use require_admin first).
8585sub start_impersonation {
8686 my ($self, $db, $dbh, $DB, $admin_session_id, $admin_user_id, $target_user_id, $mode) = @_;
8787 return 0 unless $self->_has_table($db, $dbh, $DB);
8888
8989 $admin_session_id =~ s/[^a-zA-Z0-9\-]//g;
9090 $admin_user_id =~ s/[^0-9]//g;
9191 $target_user_id =~ s/[^0-9]//g;
9292 $mode = ($mode && $mode eq 'view') ? 'view' : 'control';
9393
9494 return 0 unless $admin_session_id && $admin_user_id && $target_user_id;
9595 return 0 if $admin_user_id eq $target_user_id; # nothing to do
9696
9797 # REPLACE so re-clicking "View as X" while already acting as Y just
9898 # swaps the target rather than rejecting.
9999 $db->db_readwrite($dbh, qq~
100100 REPLACE INTO `${DB}`.admin_impersonation
101101 SET session_id = '$admin_session_id',
102102 admin_user_id = '$admin_user_id',
103103 target_user_id = '$target_user_id',
104104 mode = '$mode',
105105 started_at = NOW()
106106 ~, $ENV{SCRIPT_NAME}, __LINE__);
107107 return 1;
108108}
109109
110110# End impersonation for this admin session.
111111sub stop_impersonation {
112112 my ($self, $db, $dbh, $DB, $admin_session_id) = @_;
113113 return 0 unless $self->_has_table($db, $dbh, $DB);
114114 $admin_session_id =~ s/[^a-zA-Z0-9\-]//g;
115115 return 0 unless $admin_session_id;
116116 $db->db_readwrite($dbh, qq~
117117 DELETE FROM `${DB}`.admin_impersonation
118118 WHERE session_id='$admin_session_id'
119119 ~, $ENV{SCRIPT_NAME}, __LINE__);
120120 return 1;
121121}
122122
1231231;