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

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

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

Added
+123
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
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.
1package MODS::ContactForge::Admin;
2#======================================================================
3# ContactForge -- Admin / company-staff helpers.
4#
5# This module is the single chokepoint that enforces the line between
6# regular reps and company staff. Every admin-only endpoint must
7# call require_admin() near the top; the sidebar entry in Wrapper.pm
8# is gated on is_admin_userinfo().
9#
10# Impersonation:
11# start_impersonation($db, $dbh, $DB, $admin_session_id, $admin_user_id, $target_user_id, $mode)
12# stop_impersonation ($db, $dbh, $DB, $admin_session_id)
13#
14# The state lives in the admin_impersonation table -- the admin's
15# existing user_sessions row IS the auth principal, so an admin who
16# logs out automatically loses impersonation (CASCADE).
17#======================================================================
18use strict;
19use warnings;
20
21sub new {
22 my ($class) = @_;
23 return bless({}, $class);
24}
25
26# Is the currently authenticated principal a company admin? Returns 1
27# whether they're acting as themselves or impersonating someone else.
28# During impersonation the userinfo row is the target user's, but
29# _admin_is_admin is preserved as the admin's role flag.
30sub is_admin_userinfo {
31 my ($self, $userinfo) = @_;
32 return 0 unless $userinfo;
33 return 1 if $userinfo->{_admin_is_admin}; # impersonating
34 return 1 if $userinfo->{is_admin}; # acting as self
35 return 0;
36}
37
38# Gate an endpoint. Call before doing any admin-only work. Emits a 302
39# to /dashboard.cgi (NOT /login) so a logged-in non-admin who finds the
40# URL just lands on their own dashboard instead of being told the page
41# exists.
42sub require_admin {
43 my ($self, $userinfo) = @_;
44 return if $self->is_admin_userinfo($userinfo);
45 print "Status: 302 Found\nLocation: /dashboard.cgi\n\n";
46 exit;
47}
48
49# Real admin id even while impersonating. Use this for any audit-log
50# write or any "who pressed the button" attribution. Falls back to the
51# current user_id when not impersonating (admin acting as themselves).
52sub admin_user_id {
53 my ($self, $userinfo) = @_;
54 return 0 unless $userinfo;
55 return $userinfo->{_admin_user_id} || $userinfo->{user_id} || 0;
56}
57
58# Admin session id (the auth principal's session, NOT the impersonation
59# target's). Login.pm puts the session id on userinfo regardless of
60# impersonation state.
61sub admin_session_id {
62 my ($self, $userinfo) = @_;
63 return '' unless $userinfo;
64 my $s = $userinfo->{session_id} || '';
65 $s =~ s/[^a-zA-Z0-9\-]//g;
66 return $s;
67}
68
69# Does the admin_impersonation table exist on this DB? Many code paths
70# call this before SELECTing to avoid a 500 when the migration hasn't
71# been run yet (see feedback_guard_optional_tables memory).
72sub _has_table {
73 my ($self, $db, $dbh, $DB) = @_;
74 return $self->{_has_table} if exists $self->{_has_table};
75 my $r = $db->db_readwrite($dbh, qq~
76 SELECT COUNT(*) AS n FROM information_schema.tables
77 WHERE table_schema='$DB' AND table_name='admin_impersonation'
78 ~, $ENV{SCRIPT_NAME}, __LINE__);
79 $self->{_has_table} = ($r && $r->{n}) ? 1 : 0;
80 return $self->{_has_table};
81}
82
83# Start acting as another user. Caller is responsible for verifying
84# admin status BEFORE invoking (use require_admin first).
85sub start_impersonation {
86 my ($self, $db, $dbh, $DB, $admin_session_id, $admin_user_id, $target_user_id, $mode) = @_;
87 return 0 unless $self->_has_table($db, $dbh, $DB);
88
89 $admin_session_id =~ s/[^a-zA-Z0-9\-]//g;
90 $admin_user_id =~ s/[^0-9]//g;
91 $target_user_id =~ s/[^0-9]//g;
92 $mode = ($mode && $mode eq 'view') ? 'view' : 'control';
93
94 return 0 unless $admin_session_id && $admin_user_id && $target_user_id;
95 return 0 if $admin_user_id eq $target_user_id; # nothing to do
96
97 # REPLACE so re-clicking "View as X" while already acting as Y just
98 # swaps the target rather than rejecting.
99 $db->db_readwrite($dbh, qq~
100 REPLACE INTO `${DB}`.admin_impersonation
101 SET session_id = '$admin_session_id',
102 admin_user_id = '$admin_user_id',
103 target_user_id = '$target_user_id',
104 mode = '$mode',
105 started_at = NOW()
106 ~, $ENV{SCRIPT_NAME}, __LINE__);
107 return 1;
108}
109
110# End impersonation for this admin session.
111sub stop_impersonation {
112 my ($self, $db, $dbh, $DB, $admin_session_id) = @_;
113 return 0 unless $self->_has_table($db, $dbh, $DB);
114 $admin_session_id =~ s/[^a-zA-Z0-9\-]//g;
115 return 0 unless $admin_session_id;
116 $db->db_readwrite($dbh, qq~
117 DELETE FROM `${DB}`.admin_impersonation
118 WHERE session_id='$admin_session_id'
119 ~, $ENV{SCRIPT_NAME}, __LINE__);
120 return 1;
121}
122
1231;
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help