| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Concierge v0.6.1; |
|
2
|
7
|
|
|
7
|
|
533712
|
use v5.36; |
|
|
7
|
|
|
|
|
22
|
|
|
3
|
|
|
|
|
|
|
|
|
4
|
|
|
|
|
|
|
our $VERSION = 'v0.6.1'; |
|
5
|
|
|
|
|
|
|
|
|
6
|
|
|
|
|
|
|
# ABSTRACT: Service layer orchestrator for authentication, sessions, and user data |
|
7
|
|
|
|
|
|
|
|
|
8
|
7
|
|
|
7
|
|
44
|
use Carp qw; |
|
|
7
|
|
|
|
|
11
|
|
|
|
7
|
|
|
|
|
496
|
|
|
9
|
7
|
|
|
7
|
|
40
|
use JSON::PP qw< encode_json decode_json >; |
|
|
7
|
|
|
|
|
15
|
|
|
|
7
|
|
|
|
|
385
|
|
|
10
|
7
|
|
|
7
|
|
33
|
use File::Spec; |
|
|
7
|
|
|
|
|
28
|
|
|
|
7
|
|
|
|
|
259
|
|
|
11
|
7
|
|
|
7
|
|
4768
|
use Params::Filter qw< make_filter >; |
|
|
7
|
|
|
|
|
16537
|
|
|
|
7
|
|
|
|
|
519
|
|
|
12
|
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
# === COMPONENT MODULES === |
|
14
|
7
|
|
|
7
|
|
3741
|
use Concierge::Auth; |
|
|
7
|
|
|
|
|
207546
|
|
|
|
7
|
|
|
|
|
481
|
|
|
15
|
7
|
|
|
7
|
|
3913
|
use Concierge::Sessions; |
|
|
7
|
|
|
|
|
193158
|
|
|
|
7
|
|
|
|
|
312
|
|
|
16
|
7
|
|
|
7
|
|
3785
|
use Concierge::Users; |
|
|
7
|
|
|
|
|
124627
|
|
|
|
7
|
|
|
|
|
343
|
|
|
17
|
7
|
|
|
7
|
|
3704
|
use Concierge::User; |
|
|
7
|
|
|
|
|
17
|
|
|
|
7
|
|
|
|
|
27170
|
|
|
18
|
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
# === PARAMETER FILTERS === |
|
20
|
|
|
|
|
|
|
# Shared filters for secure data segregation |
|
21
|
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
# Auth filter - ONLY credentials (user_id + password) |
|
23
|
|
|
|
|
|
|
our $auth_data_filter = make_filter( |
|
24
|
|
|
|
|
|
|
[qw(user_id password)], # required credentials |
|
25
|
|
|
|
|
|
|
[], # accepted - nothing else |
|
26
|
|
|
|
|
|
|
[], # excluded - not needed |
|
27
|
|
|
|
|
|
|
); |
|
28
|
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
# User data filter - everything EXCEPT credentials |
|
30
|
|
|
|
|
|
|
# Handles both minimal input (user_id, moniker) and |
|
31
|
|
|
|
|
|
|
# rich input (user_id, moniker, email, phone, bio, etc.) |
|
32
|
|
|
|
|
|
|
our $user_data_filter = make_filter( |
|
33
|
|
|
|
|
|
|
[qw(user_id moniker)], # required minimum |
|
34
|
|
|
|
|
|
|
['*'], # accept ALL other fields, except: |
|
35
|
|
|
|
|
|
|
[qw(password confirm_password)], # excluded - security boundary |
|
36
|
|
|
|
|
|
|
); |
|
37
|
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
# Session data filter - for populating session with initial data |
|
39
|
|
|
|
|
|
|
# Accepts user_id (required for new_session) plus any session fields |
|
40
|
|
|
|
|
|
|
# Excludes credentials (never stored in session data) |
|
41
|
|
|
|
|
|
|
our $session_data_filter = make_filter( |
|
42
|
|
|
|
|
|
|
[qw(user_id)], # required for new_session |
|
43
|
|
|
|
|
|
|
['*'], # accept all other fields, except: |
|
44
|
|
|
|
|
|
|
[qw(password confirm_password)], # excluded - security boundary |
|
45
|
|
|
|
|
|
|
); |
|
46
|
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
# User update filter - for updating existing user records |
|
48
|
|
|
|
|
|
|
# No required fields (user_id passed separately as parameter) |
|
49
|
|
|
|
|
|
|
# Excludes user_id (identity field), password (use reset_password instead) |
|
50
|
|
|
|
|
|
|
our $user_update_filter = make_filter( |
|
51
|
|
|
|
|
|
|
[], # no required fields |
|
52
|
|
|
|
|
|
|
['*'], # accept all fields, except: |
|
53
|
|
|
|
|
|
|
[qw(user_id password confirm_password)], # excluded - never in updates |
|
54
|
|
|
|
|
|
|
); |
|
55
|
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
sub new_concierge { |
|
57
|
13
|
|
|
13
|
0
|
43
|
my ($class) = @_; |
|
58
|
13
|
|
|
|
|
48
|
bless {}, $class; |
|
59
|
|
|
|
|
|
|
} |
|
60
|
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
# ============================================================================= |
|
62
|
|
|
|
|
|
|
# DESK MANAGEMENT - Opening existing desks |
|
63
|
|
|
|
|
|
|
# ============================================================================= |
|
64
|
|
|
|
|
|
|
|
|
65
|
14
|
|
|
14
|
1
|
19506
|
sub open_desk ($class, $desk_location) { |
|
|
14
|
|
|
|
|
38
|
|
|
|
14
|
|
|
|
|
31
|
|
|
|
14
|
|
|
|
|
26
|
|
|
66
|
14
|
100
|
|
|
|
465
|
croak "Desk not found: *$desk_location*" unless -d $desk_location; |
|
67
|
13
|
|
|
|
|
94
|
my $concierge = Concierge->new_concierge(); # minimal object |
|
68
|
13
|
|
|
|
|
68
|
$concierge->{desk_location} = $desk_location; |
|
69
|
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
# Instantiate the concierge from config stored in Concierge's config file |
|
71
|
13
|
|
|
|
|
238
|
my $concierge_conf_file = File::Spec->catfile($desk_location, 'concierge.conf'); |
|
72
|
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
# Read entire file (pretty JSON spans multiple lines) |
|
74
|
13
|
|
|
|
|
52
|
my ($fh,$json,$concierge_config); |
|
75
|
|
|
|
|
|
|
{ |
|
76
|
13
|
|
|
|
|
21
|
local $/; |
|
|
13
|
|
|
|
|
73
|
|
|
77
|
13
|
50
|
33
|
|
|
1477
|
open $fh, "<", $concierge_conf_file |
|
|
|
|
33
|
|
|
|
|
|
78
|
|
|
|
|
|
|
and |
|
79
|
|
|
|
|
|
|
$json = <$fh> |
|
80
|
|
|
|
|
|
|
and |
|
81
|
|
|
|
|
|
|
close $fh |
|
82
|
|
|
|
|
|
|
or return { success => 0, message => "Error closing session file: $!" }; |
|
83
|
|
|
|
|
|
|
} |
|
84
|
13
|
50
|
|
|
|
71
|
unless (defined $json) { |
|
85
|
0
|
|
|
|
|
0
|
return { success => 0, message => "Config file is empty" }; |
|
86
|
|
|
|
|
|
|
} |
|
87
|
13
|
|
|
|
|
27
|
eval { |
|
88
|
13
|
|
|
|
|
101
|
$concierge_config = decode_json($json); |
|
89
|
|
|
|
|
|
|
}; |
|
90
|
13
|
50
|
|
|
|
42081
|
if ($@) { |
|
91
|
0
|
|
|
|
|
0
|
return { success => 0, message => "Invalid JSON in config file: $@" }; |
|
92
|
|
|
|
|
|
|
} |
|
93
|
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
# Instantiate sessions manager from $concierge_config |
|
95
|
|
|
|
|
|
|
$concierge->{sessions} = Concierge::Sessions->new( |
|
96
|
|
|
|
|
|
|
storage_dir => $concierge_config->{sessions_dir} || $concierge_config->{storage_dir}, |
|
97
|
13
|
|
33
|
|
|
242
|
backend => $concierge_config->{sessions_backend} || '', |
|
|
|
|
50
|
|
|
|
|
|
98
|
|
|
|
|
|
|
); |
|
99
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
# Load user_keys mapping from file (or initialize empty for new desk) |
|
101
|
13
|
|
|
|
|
15631
|
my $user_keys_file = File::Spec->catfile($desk_location, 'user_keys.json'); |
|
102
|
13
|
100
|
|
|
|
663
|
if (-e $user_keys_file) { |
|
103
|
2
|
|
|
|
|
12
|
local $/; |
|
104
|
2
|
|
|
|
|
4
|
my $user_keys_fh; |
|
105
|
2
|
50
|
|
|
|
114
|
open $user_keys_fh, "<", $user_keys_file |
|
106
|
|
|
|
|
|
|
or return { success => 0, message => "Cannot read user_keys file: $!" }; |
|
107
|
2
|
|
|
|
|
62
|
my $user_keys_json = <$user_keys_fh>; |
|
108
|
2
|
|
|
|
|
29
|
close $user_keys_fh; |
|
109
|
2
|
|
|
|
|
15
|
$concierge->{user_keys} = decode_json($user_keys_json); |
|
110
|
|
|
|
|
|
|
} else { |
|
111
|
11
|
|
|
|
|
49
|
$concierge->{user_keys} = {}; # New desk or first run |
|
112
|
|
|
|
|
|
|
} |
|
113
|
|
|
|
|
|
|
|
|
114
|
|
|
|
|
|
|
# Cleanup expired sessions and synchronize user_keys mapping |
|
115
|
13
|
|
|
|
|
5282
|
my $cleanup_result = $concierge->{sessions}->cleanup_sessions(); |
|
116
|
|
|
|
|
|
|
# Returns: { success => 1, deleted_count => N, active => [session_ids...] } |
|
117
|
|
|
|
|
|
|
|
|
118
|
13
|
50
|
33
|
|
|
6128
|
if ($cleanup_result->{success} && $cleanup_result->{active}) { |
|
119
|
|
|
|
|
|
|
# Create lookup hash of active session_ids |
|
120
|
13
|
|
|
|
|
37
|
my %active_sessions = map { $_ => 1 } @{$cleanup_result->{active}}; |
|
|
3
|
|
|
|
|
31
|
|
|
|
13
|
|
|
|
|
58
|
|
|
121
|
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
# Remove user_keys entries for deleted sessions |
|
123
|
13
|
|
|
|
|
31
|
my $cleaned = 0; |
|
124
|
13
|
|
|
|
|
23
|
for my $key (keys %{$concierge->{user_keys}}) { |
|
|
13
|
|
|
|
|
74
|
|
|
125
|
4
|
|
|
|
|
11
|
my $session_id = $concierge->{user_keys}{$key}{session_id}; |
|
126
|
4
|
100
|
|
|
|
288
|
unless ($active_sessions{$session_id}) { |
|
127
|
1
|
|
|
|
|
7
|
delete $concierge->{user_keys}{$key}; |
|
128
|
1
|
|
|
|
|
3
|
$cleaned++; |
|
129
|
|
|
|
|
|
|
} |
|
130
|
|
|
|
|
|
|
} |
|
131
|
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
# Save cleaned mapping if any were removed |
|
133
|
13
|
100
|
|
|
|
105
|
if ($cleaned > 0) { |
|
134
|
1
|
|
|
|
|
6
|
$concierge->save_user_keys(); |
|
135
|
|
|
|
|
|
|
} |
|
136
|
|
|
|
|
|
|
} |
|
137
|
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
# Instantiate users and auth from $concierge_config |
|
139
|
13
|
|
|
|
|
150
|
$concierge->{users} = Concierge::Users->new( $concierge_config->{users_config_file} ); |
|
140
|
13
|
|
|
|
|
1574898
|
$concierge->{auth} = Concierge::Auth->new( { file => $concierge_config->{auth_file} } ); |
|
141
|
|
|
|
|
|
|
|
|
142
|
13
|
|
|
|
|
4117
|
return { success => 1, message => 'Welcome!', concierge => $concierge }; |
|
143
|
|
|
|
|
|
|
} |
|
144
|
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
sub sessions { |
|
146
|
38
|
|
|
38
|
0
|
4649
|
$_[0]->{sessions}; |
|
147
|
|
|
|
|
|
|
} |
|
148
|
|
|
|
|
|
|
sub users { |
|
149
|
53
|
|
|
53
|
0
|
585
|
$_[0]->{users}; |
|
150
|
|
|
|
|
|
|
} |
|
151
|
|
|
|
|
|
|
sub auth { |
|
152
|
40
|
|
|
40
|
0
|
418
|
$_[0]->{auth}; |
|
153
|
|
|
|
|
|
|
} |
|
154
|
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
# ============================================================================= |
|
156
|
|
|
|
|
|
|
# CONCIERGE STATE MANAGEMENT |
|
157
|
|
|
|
|
|
|
# ============================================================================= |
|
158
|
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
# Save user_keys mapping to persistent storage |
|
160
|
26
|
|
|
26
|
0
|
54
|
sub save_user_keys ($self) { |
|
|
26
|
|
|
|
|
68
|
|
|
|
26
|
|
|
|
|
39
|
|
|
161
|
26
|
|
|
|
|
922
|
my $user_keys_file = File::Spec->catfile($self->{desk_location}, 'user_keys.json'); |
|
162
|
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
# my $json = encode_json($self->{user_keys}); |
|
164
|
26
|
|
|
|
|
207
|
my $json = JSON::PP->new->utf8->pretty->encode( $self->{user_keys} ); |
|
165
|
26
|
50
|
|
|
|
24833
|
open my $fh, ">", $user_keys_file |
|
166
|
|
|
|
|
|
|
or return { success => 0, message => "Cannot write user_keys file: $!" }; |
|
167
|
26
|
|
|
|
|
352
|
print $fh $json; |
|
168
|
26
|
50
|
|
|
|
4955
|
close $fh |
|
169
|
|
|
|
|
|
|
or return { success => 0, message => "Error closing user_keys file: $!" }; |
|
170
|
|
|
|
|
|
|
|
|
171
|
26
|
|
|
|
|
344
|
return { success => 1 }; |
|
172
|
|
|
|
|
|
|
} |
|
173
|
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
|
|
175
|
|
|
|
|
|
|
# ============================================================================= |
|
176
|
|
|
|
|
|
|
# USER MANAGEMENT AND AUTHENTICATION |
|
177
|
|
|
|
|
|
|
# ============================================================================= |
|
178
|
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
# Add user: register in Users, set password in Auth |
|
180
|
17
|
|
|
17
|
1
|
32600
|
sub add_user ($self, $user_input) { |
|
|
17
|
|
|
|
|
38
|
|
|
|
17
|
|
|
|
|
24
|
|
|
|
17
|
|
|
|
|
24
|
|
|
181
|
|
|
|
|
|
|
# Single input hashref handles both minimal and rich input: |
|
182
|
|
|
|
|
|
|
# Minimal: { user_id => '...', moniker => '...', password => '...' } |
|
183
|
|
|
|
|
|
|
# Rich: { user_id => '...', moniker => '...', email => '...', |
|
184
|
|
|
|
|
|
|
# phone => '...', bio => '...', password => '...' } |
|
185
|
|
|
|
|
|
|
|
|
186
|
17
|
50
|
|
|
|
64
|
return { success => 0, message => 'user_input must be a hash reference' } |
|
187
|
|
|
|
|
|
|
unless ref $user_input eq 'HASH'; |
|
188
|
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
# Filter for Users - gets everything EXCEPT password |
|
190
|
17
|
|
|
|
|
86
|
my $user_data = $user_data_filter->($user_input); |
|
191
|
17
|
100
|
|
|
|
585
|
return { success => 0, message => 'Missing required fields: user_id and moniker' } |
|
192
|
|
|
|
|
|
|
unless $user_data; |
|
193
|
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
# Filter for Auth - gets ONLY user_id and password |
|
195
|
15
|
|
|
|
|
67
|
my $auth_data = $auth_data_filter->($user_input); |
|
196
|
15
|
100
|
|
|
|
2684
|
return { success => 0, message => 'Missing required field: password' } |
|
197
|
|
|
|
|
|
|
unless $auth_data; |
|
198
|
|
|
|
|
|
|
|
|
199
|
14
|
|
|
|
|
48
|
my $user_id = $auth_data->{user_id}; |
|
200
|
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
# Step 1: Register user in Users component (password automatically excluded) |
|
202
|
14
|
|
|
|
|
79
|
my $register_result = $self->users->register_user($user_data); |
|
203
|
14
|
100
|
|
|
|
307152
|
unless ($register_result->{success}) { |
|
204
|
1
|
|
|
|
|
8
|
return { success => 0, message => $register_result->{message} }; |
|
205
|
|
|
|
|
|
|
} |
|
206
|
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
# Step 2: Set password in Auth component |
|
208
|
13
|
|
|
|
|
97
|
my ($pwd_ok, $pwd_msg) = $self->auth->setPwd($user_id, $auth_data->{password}); |
|
209
|
13
|
50
|
|
|
|
18728595
|
unless ($pwd_ok) { |
|
210
|
|
|
|
|
|
|
# Rollback: delete the user record since password failed |
|
211
|
0
|
|
|
|
|
0
|
$self->users->delete_user($user_id); |
|
212
|
0
|
|
0
|
|
|
0
|
return { success => 0, message => $pwd_msg || 'Failed to set password' }; |
|
213
|
|
|
|
|
|
|
} |
|
214
|
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
return { |
|
216
|
13
|
|
|
|
|
271
|
success => 1, |
|
217
|
|
|
|
|
|
|
message => "User '$user_id' added successfully", |
|
218
|
|
|
|
|
|
|
user_id => $user_id, |
|
219
|
|
|
|
|
|
|
}; |
|
220
|
|
|
|
|
|
|
} |
|
221
|
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
# Remove user: delete from all components (Auth, Users, Sessions, concierge mapping) |
|
223
|
1
|
|
|
1
|
1
|
79
|
sub remove_user ($self, $user_id) { |
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
1
|
|
|
224
|
|
|
|
|
|
|
# Removes user from all Concierge components |
|
225
|
|
|
|
|
|
|
# Attempts all deletions with graceful handling |
|
226
|
|
|
|
|
|
|
# Application should handle higher-level cleanup (files, assets) before calling this |
|
227
|
|
|
|
|
|
|
|
|
228
|
1
|
50
|
33
|
|
|
9
|
return { success => 0, message => 'user_id is required' } |
|
229
|
|
|
|
|
|
|
unless defined $user_id && length($user_id); |
|
230
|
|
|
|
|
|
|
|
|
231
|
1
|
|
|
|
|
958
|
my @deleted_from; |
|
232
|
|
|
|
|
|
|
my @warnings; |
|
233
|
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
# Step 1: Delete from Users component |
|
235
|
1
|
|
|
|
|
9
|
my $users_result = $self->users->delete_user($user_id); |
|
236
|
1
|
50
|
|
|
|
47849
|
if ($users_result->{success}) { |
|
237
|
1
|
|
|
|
|
5
|
push @deleted_from, 'Users'; |
|
238
|
|
|
|
|
|
|
} else { |
|
239
|
0
|
|
|
|
|
0
|
push @warnings, "Users: $users_result->{message}"; |
|
240
|
|
|
|
|
|
|
} |
|
241
|
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
# Step 2: Delete from Auth component |
|
243
|
1
|
|
|
|
|
23
|
my ($auth_ok, $auth_msg) = $self->auth->deleteID($user_id); |
|
244
|
1
|
50
|
|
|
|
463
|
if ($auth_ok) { |
|
245
|
1
|
|
|
|
|
21
|
push @deleted_from, 'Auth'; |
|
246
|
|
|
|
|
|
|
} else { |
|
247
|
0
|
|
0
|
|
|
0
|
push @warnings, "Auth: " . ($auth_msg || 'deletion failed'); |
|
248
|
|
|
|
|
|
|
} |
|
249
|
|
|
|
|
|
|
|
|
250
|
|
|
|
|
|
|
# Step 3: Find and delete session (if user has active session) |
|
251
|
1
|
|
|
|
|
12
|
my $session_id; |
|
252
|
|
|
|
|
|
|
my $user_key_to_delete; |
|
253
|
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
# Search user_keys mapping for this user_id |
|
255
|
1
|
|
|
|
|
3
|
for my $key (keys %{$self->{user_keys}}) { |
|
|
1
|
|
|
|
|
7
|
|
|
256
|
1
|
50
|
|
|
|
8
|
if ($self->{user_keys}{$key}{user_id} eq $user_id) { |
|
257
|
1
|
|
|
|
|
3
|
$session_id = $self->{user_keys}{$key}{session_id}; |
|
258
|
1
|
|
|
|
|
221
|
$user_key_to_delete = $key; |
|
259
|
1
|
|
|
|
|
6
|
last; |
|
260
|
|
|
|
|
|
|
} |
|
261
|
|
|
|
|
|
|
} |
|
262
|
|
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
# Delete session if found |
|
264
|
1
|
50
|
|
|
|
6
|
if ($session_id) { |
|
265
|
1
|
|
|
|
|
6
|
my $session_result = $self->sessions->delete_session($session_id); |
|
266
|
1
|
50
|
|
|
|
24053
|
if ($session_result->{success}) { |
|
267
|
1
|
|
|
|
|
6
|
push @deleted_from, 'Sessions'; |
|
268
|
|
|
|
|
|
|
} else { |
|
269
|
0
|
|
|
|
|
0
|
push @warnings, "Sessions: $session_result->{message}"; |
|
270
|
|
|
|
|
|
|
} |
|
271
|
|
|
|
|
|
|
} |
|
272
|
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
# Step 4: Remove from concierge user_keys mapping |
|
274
|
1
|
50
|
|
|
|
5
|
if ($user_key_to_delete) { |
|
275
|
1
|
|
|
|
|
9
|
delete $self->{user_keys}{$user_key_to_delete}; |
|
276
|
1
|
|
|
|
|
6
|
my $save_result = $self->save_user_keys(); |
|
277
|
1
|
50
|
|
|
|
9
|
if ($save_result->{success}) { |
|
278
|
1
|
|
|
|
|
5
|
push @deleted_from, 'concierge mapping'; |
|
279
|
|
|
|
|
|
|
} else { |
|
280
|
0
|
|
|
|
|
0
|
push @warnings, "Concierge mapping: failed to save"; |
|
281
|
|
|
|
|
|
|
} |
|
282
|
|
|
|
|
|
|
} |
|
283
|
|
|
|
|
|
|
|
|
284
|
|
|
|
|
|
|
# Build response |
|
285
|
1
|
50
|
|
|
|
9
|
my $message = scalar(@deleted_from) |
|
286
|
|
|
|
|
|
|
? "User '$user_id' removed from: " . join(', ', @deleted_from) |
|
287
|
|
|
|
|
|
|
: "User '$user_id' not found in any component"; |
|
288
|
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
return { |
|
290
|
1
|
50
|
|
|
|
14
|
success => 1, |
|
291
|
|
|
|
|
|
|
message => $message, |
|
292
|
|
|
|
|
|
|
user_id => $user_id, |
|
293
|
|
|
|
|
|
|
deleted_from => \@deleted_from, |
|
294
|
|
|
|
|
|
|
(@warnings ? (warnings => \@warnings) : ()), |
|
295
|
|
|
|
|
|
|
}; |
|
296
|
|
|
|
|
|
|
} |
|
297
|
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
# Verify user: check if user exists in both Auth and Users components |
|
299
|
3
|
|
|
3
|
1
|
9118
|
sub verify_user ($self, $user_id) { |
|
|
3
|
|
|
|
|
11
|
|
|
|
3
|
|
|
|
|
5
|
|
|
|
3
|
|
|
|
|
6
|
|
|
300
|
|
|
|
|
|
|
# Verifies user exists and checks for data consistency |
|
301
|
|
|
|
|
|
|
# User is considered verified only if present in BOTH components |
|
302
|
|
|
|
|
|
|
|
|
303
|
3
|
50
|
33
|
|
|
26
|
return { success => 0, message => 'user_id is required' } |
|
304
|
|
|
|
|
|
|
unless defined $user_id && length($user_id); |
|
305
|
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
# Check Auth component (password file) |
|
307
|
3
|
|
|
|
|
16
|
my $auth_exists = $self->auth->checkID($user_id); |
|
308
|
|
|
|
|
|
|
|
|
309
|
|
|
|
|
|
|
# Check Users component (user data store) |
|
310
|
3
|
|
|
|
|
893
|
my $user_result = $self->users->get_user($user_id); |
|
311
|
3
|
|
|
|
|
1114
|
my $users_exists = $user_result->{success}; |
|
312
|
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
# User is verified if in BOTH components |
|
314
|
3
|
|
66
|
|
|
14
|
my $verified = $auth_exists && $users_exists; |
|
315
|
|
|
|
|
|
|
|
|
316
|
3
|
|
|
|
|
40
|
my $response = { |
|
317
|
|
|
|
|
|
|
success => 1, |
|
318
|
|
|
|
|
|
|
verified => $verified, |
|
319
|
|
|
|
|
|
|
exists_in_auth => $auth_exists, |
|
320
|
|
|
|
|
|
|
exists_in_users => $users_exists, |
|
321
|
|
|
|
|
|
|
}; |
|
322
|
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
# Add user_status if available |
|
324
|
3
|
50
|
66
|
|
|
16
|
if ($users_exists && exists $user_result->{user}{user_status}) { |
|
325
|
1
|
|
|
|
|
4
|
$response->{user_status} = $user_result->{user}{user_status}; |
|
326
|
|
|
|
|
|
|
} |
|
327
|
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
# Warn if inconsistent (exists in one but not both) |
|
329
|
3
|
50
|
|
|
|
35
|
if ($auth_exists != $users_exists) { |
|
330
|
0
|
|
|
|
|
0
|
$response->{warning} = 'User exists in only one component - data inconsistency detected'; |
|
331
|
|
|
|
|
|
|
} |
|
332
|
|
|
|
|
|
|
|
|
333
|
3
|
|
|
|
|
17
|
return $response; |
|
334
|
|
|
|
|
|
|
} |
|
335
|
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
# Update user data: modify user record in Users component |
|
337
|
2
|
|
|
2
|
1
|
8831
|
sub update_user_data ($self, $user_id, $update_data) { |
|
|
2
|
|
|
|
|
4
|
|
|
|
2
|
|
|
|
|
4
|
|
|
|
2
|
|
|
|
|
3
|
|
|
|
2
|
|
|
|
|
3
|
|
|
338
|
2
|
50
|
33
|
|
|
12
|
return { success => 0, message => 'user_id is required' } |
|
339
|
|
|
|
|
|
|
unless defined $user_id && length($user_id); |
|
340
|
|
|
|
|
|
|
|
|
341
|
2
|
50
|
|
|
|
8
|
return { success => 0, message => 'update_data must be a hash reference' } |
|
342
|
|
|
|
|
|
|
unless ref $update_data eq 'HASH'; |
|
343
|
|
|
|
|
|
|
|
|
344
|
|
|
|
|
|
|
# Filter update data (excludes user_id, password, confirm_password) |
|
345
|
2
|
|
|
|
|
13
|
my $filtered_updates = $user_update_filter->($update_data); |
|
346
|
2
|
50
|
33
|
|
|
57
|
return { success => 0, message => 'No valid fields to update' } |
|
347
|
|
|
|
|
|
|
unless $filtered_updates && keys %$filtered_updates; |
|
348
|
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
# Update user in Users component |
|
350
|
2
|
|
|
|
|
6
|
my $update_result = $self->users->update_user($user_id, $filtered_updates); |
|
351
|
2
|
50
|
|
|
|
36095
|
unless ($update_result->{success}) { |
|
352
|
0
|
|
|
|
|
0
|
return { success => 0, message => $update_result->{message} }; |
|
353
|
|
|
|
|
|
|
} |
|
354
|
|
|
|
|
|
|
|
|
355
|
|
|
|
|
|
|
return { |
|
356
|
2
|
|
|
|
|
27
|
success => 1, |
|
357
|
|
|
|
|
|
|
message => "User '$user_id' updated successfully", |
|
358
|
|
|
|
|
|
|
user_id => $user_id, |
|
359
|
|
|
|
|
|
|
}; |
|
360
|
|
|
|
|
|
|
} |
|
361
|
|
|
|
|
|
|
|
|
362
|
|
|
|
|
|
|
# Get user data: retrieve user profile from Users component |
|
363
|
7
|
|
|
7
|
1
|
6562
|
sub get_user_data ($self, $user_id, @fields) { |
|
|
7
|
|
|
|
|
12
|
|
|
|
7
|
|
|
|
|
15
|
|
|
|
7
|
|
|
|
|
9
|
|
|
|
7
|
|
|
|
|
8
|
|
|
364
|
|
|
|
|
|
|
# Retrieves user profile data fields |
|
365
|
|
|
|
|
|
|
# If @fields specified, returns only those fields |
|
366
|
|
|
|
|
|
|
# Otherwise returns all user data |
|
367
|
|
|
|
|
|
|
|
|
368
|
7
|
50
|
33
|
|
|
39
|
return { success => 0, message => 'user_id is required' } |
|
369
|
|
|
|
|
|
|
unless defined $user_id && length($user_id); |
|
370
|
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
# Get user from Users component |
|
372
|
7
|
|
|
|
|
22
|
my $user_result = $self->users->get_user($user_id); |
|
373
|
|
|
|
|
|
|
return { success => 0, message => $user_result->{message} || 'User not found' } |
|
374
|
7
|
50
|
0
|
|
|
2208
|
unless $user_result->{success}; |
|
375
|
|
|
|
|
|
|
|
|
376
|
7
|
|
|
|
|
41
|
my $user_data = $user_result->{user}; |
|
377
|
|
|
|
|
|
|
|
|
378
|
|
|
|
|
|
|
# If specific fields requested, return only those that exist |
|
379
|
7
|
100
|
|
|
|
19
|
if (@fields) { |
|
380
|
1
|
|
|
|
|
2
|
my %selected; |
|
381
|
1
|
|
|
|
|
3
|
for my $field (@fields) { |
|
382
|
2
|
50
|
|
|
|
7
|
$selected{$field} = $user_data->{$field} if exists $user_data->{$field}; |
|
383
|
|
|
|
|
|
|
} |
|
384
|
1
|
|
|
|
|
9
|
return { success => 1, user => \%selected }; |
|
385
|
|
|
|
|
|
|
} |
|
386
|
|
|
|
|
|
|
|
|
387
|
|
|
|
|
|
|
# Otherwise return all data |
|
388
|
6
|
|
|
|
|
62
|
return { success => 1, user => $user_data }; |
|
389
|
|
|
|
|
|
|
} |
|
390
|
|
|
|
|
|
|
|
|
391
|
|
|
|
|
|
|
# List users: retrieve list of user_ids, optionally with full data |
|
392
|
2
|
|
|
2
|
1
|
2513
|
sub list_users ($self, $filter='', $options={}) { |
|
|
2
|
|
|
|
|
3
|
|
|
|
2
|
|
|
|
|
3
|
|
|
|
2
|
|
|
|
|
4
|
|
|
|
2
|
|
|
|
|
3
|
|
|
393
|
|
|
|
|
|
|
# Returns user_ids by default |
|
394
|
|
|
|
|
|
|
# With include_data => 1: fetches full user records |
|
395
|
|
|
|
|
|
|
# With fields => [...]: returns only specified fields per user |
|
396
|
|
|
|
|
|
|
|
|
397
|
|
|
|
|
|
|
# Get user_ids from Users component |
|
398
|
2
|
|
|
|
|
7
|
my $list_result = $self->users->list_users($filter); |
|
399
|
|
|
|
|
|
|
return { success => 0, message => $list_result->{message} || 'Failed to list users' } |
|
400
|
2
|
50
|
0
|
|
|
713
|
unless $list_result->{success}; |
|
401
|
|
|
|
|
|
|
|
|
402
|
2
|
|
50
|
|
|
6
|
my $user_ids = $list_result->{user_ids} // []; |
|
403
|
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
# If only IDs requested, return them with count |
|
405
|
2
|
100
|
|
|
|
5
|
unless ($options->{include_data}) { |
|
406
|
|
|
|
|
|
|
return { |
|
407
|
1
|
|
|
|
|
6
|
success => 1, |
|
408
|
|
|
|
|
|
|
user_ids => $user_ids, |
|
409
|
|
|
|
|
|
|
count => scalar(@$user_ids), |
|
410
|
|
|
|
|
|
|
}; |
|
411
|
|
|
|
|
|
|
} |
|
412
|
|
|
|
|
|
|
|
|
413
|
|
|
|
|
|
|
# Fetch full data for each user |
|
414
|
1
|
|
|
|
|
3
|
my %users; # Hash indexed by user_id for easy lookup |
|
415
|
1
|
50
|
|
|
|
3
|
my @fields = $options->{fields} ? $options->{fields}->@* : (); |
|
416
|
|
|
|
|
|
|
|
|
417
|
1
|
|
|
|
|
3
|
for my $user_id (@$user_ids) { |
|
418
|
3
|
|
|
|
|
7
|
my $data = $self->get_user_data($user_id, @fields); |
|
419
|
3
|
50
|
|
|
|
27
|
$users{$user_id} = $data->{user} if $data->{success}; |
|
420
|
|
|
|
|
|
|
} |
|
421
|
|
|
|
|
|
|
|
|
422
|
|
|
|
|
|
|
return { |
|
423
|
1
|
|
|
|
|
6
|
success => 1, |
|
424
|
|
|
|
|
|
|
user_ids => $user_ids, # Preserve order |
|
425
|
|
|
|
|
|
|
users => \%users, # Hash for easy lookup |
|
426
|
|
|
|
|
|
|
count => scalar(keys %users), |
|
427
|
|
|
|
|
|
|
}; |
|
428
|
|
|
|
|
|
|
} |
|
429
|
|
|
|
|
|
|
|
|
430
|
|
|
|
|
|
|
# Build closures that give a User object backend access to its data store |
|
431
|
|
|
|
|
|
|
# Called by any Concierge method that creates a logged-in User object |
|
432
|
18
|
|
|
18
|
|
129
|
sub _make_user_closures ($self, $user_id) { |
|
|
18
|
|
|
|
|
44
|
|
|
|
18
|
|
|
|
|
34
|
|
|
|
18
|
|
|
|
|
36
|
|
|
433
|
2
|
|
|
2
|
|
4
|
my $get_user_data = sub (@fields) { |
|
|
2
|
|
|
|
|
20
|
|
|
|
2
|
|
|
|
|
5
|
|
|
434
|
2
|
|
|
|
|
10
|
my $result = $self->users->get_user($user_id); |
|
435
|
2
|
50
|
|
|
|
895
|
return $result unless $result->{success}; |
|
436
|
|
|
|
|
|
|
|
|
437
|
2
|
50
|
|
|
|
8
|
if (@fields) { |
|
438
|
0
|
|
|
|
|
0
|
my %selected = map { $_ => $result->{user}{$_} } |
|
439
|
0
|
|
|
|
|
0
|
grep { exists $result->{user}{$_} } |
|
|
0
|
|
|
|
|
0
|
|
|
440
|
|
|
|
|
|
|
@fields; |
|
441
|
0
|
|
|
|
|
0
|
return { success => 1, user => \%selected }; |
|
442
|
|
|
|
|
|
|
} |
|
443
|
2
|
|
|
|
|
8
|
return $result; |
|
444
|
18
|
|
|
|
|
159
|
}; |
|
445
|
|
|
|
|
|
|
|
|
446
|
2
|
|
|
2
|
|
4
|
my $update_user_data = sub ($updates) { |
|
|
2
|
|
|
|
|
4
|
|
|
|
2
|
|
|
|
|
4
|
|
|
447
|
2
|
|
|
|
|
8
|
return $self->users->update_user($user_id, $updates); |
|
448
|
18
|
|
|
|
|
81
|
}; |
|
449
|
|
|
|
|
|
|
|
|
450
|
18
|
|
|
|
|
64
|
return ($get_user_data, $update_user_data); |
|
451
|
|
|
|
|
|
|
} |
|
452
|
|
|
|
|
|
|
|
|
453
|
|
|
|
|
|
|
# Admit visitor: assign user_key only (no session, no user data) |
|
454
|
1
|
|
|
1
|
1
|
6000
|
sub admit_visitor ($self) { |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
2
|
|
|
455
|
1
|
50
|
|
|
|
7
|
my $visitor_id = Concierge::Auth->gen_random_string(13) |
|
456
|
|
|
|
|
|
|
or return { success => 0, message => "Couldn't generate visitor ID" }; |
|
457
|
|
|
|
|
|
|
|
|
458
|
|
|
|
|
|
|
# Create user object for visitor (no session, no user_data, no backend access) |
|
459
|
1
|
|
|
|
|
405
|
my $user = Concierge::User->enable_user($visitor_id, { |
|
460
|
|
|
|
|
|
|
user_key => $visitor_id, # Use visitor_id as user_key |
|
461
|
|
|
|
|
|
|
}); |
|
462
|
|
|
|
|
|
|
|
|
463
|
|
|
|
|
|
|
return { |
|
464
|
1
|
|
|
|
|
7
|
success => 1, |
|
465
|
|
|
|
|
|
|
user => $user, |
|
466
|
|
|
|
|
|
|
is_visitor => 1, |
|
467
|
|
|
|
|
|
|
}; |
|
468
|
|
|
|
|
|
|
} |
|
469
|
|
|
|
|
|
|
|
|
470
|
|
|
|
|
|
|
# Checkin guest: assign a session with no user data backend or authentication |
|
471
|
4
|
|
|
4
|
1
|
12430
|
sub checkin_guest ($self, $session_opts={}) { |
|
|
4
|
|
|
|
|
8
|
|
|
|
4
|
|
|
|
|
10
|
|
|
|
4
|
|
|
|
|
6
|
|
|
472
|
4
|
50
|
|
|
|
34
|
my $guest_id = Concierge::Auth->gen_random_string(13) |
|
473
|
|
|
|
|
|
|
or return { success => 0, message => "Couldn't generate guest ID" }; |
|
474
|
|
|
|
|
|
|
|
|
475
|
|
|
|
|
|
|
# Shorter timeout for anonymous |
|
476
|
|
|
|
|
|
|
my $timeout = $session_opts->{timeout} |
|
477
|
|
|
|
|
|
|
// $self->{config}{anonymous_timeout} |
|
478
|
4
|
|
33
|
|
|
847
|
// 1800; # 30 minutes default |
|
|
|
|
50
|
|
|
|
|
|
479
|
|
|
|
|
|
|
|
|
480
|
|
|
|
|
|
|
# Create session using Sessions manager |
|
481
|
4
|
|
|
|
|
19
|
my $result = $self->sessions->new_session( |
|
482
|
|
|
|
|
|
|
user_id => $guest_id, |
|
483
|
|
|
|
|
|
|
session_timeout => $timeout, |
|
484
|
|
|
|
|
|
|
); |
|
485
|
|
|
|
|
|
|
return { success => 0, message => $result->{message} } |
|
486
|
4
|
50
|
|
|
|
62504
|
unless $result->{success}; |
|
487
|
|
|
|
|
|
|
|
|
488
|
4
|
|
|
|
|
71
|
my $session = $result->{session}; |
|
489
|
4
|
|
|
|
|
21
|
my $session_id = $session->session_id(); |
|
490
|
|
|
|
|
|
|
|
|
491
|
|
|
|
|
|
|
# Create user object for guest (no user_data, no backend closures) |
|
492
|
4
|
|
|
|
|
55
|
my $user = Concierge::User->enable_user($guest_id, { |
|
493
|
|
|
|
|
|
|
session => $session, |
|
494
|
|
|
|
|
|
|
user_key => $guest_id, # Use guest_id as user_key for simplicity |
|
495
|
|
|
|
|
|
|
}); |
|
496
|
|
|
|
|
|
|
|
|
497
|
|
|
|
|
|
|
# Store user_key mapping |
|
498
|
4
|
|
|
|
|
30
|
$self->{user_keys}{$user->user_key()} = { |
|
499
|
|
|
|
|
|
|
user_id => $guest_id, |
|
500
|
|
|
|
|
|
|
session_id => $session_id, |
|
501
|
|
|
|
|
|
|
}; |
|
502
|
4
|
|
|
|
|
21
|
$self->save_user_keys(); |
|
503
|
|
|
|
|
|
|
|
|
504
|
|
|
|
|
|
|
return { |
|
505
|
4
|
|
|
|
|
34
|
success => 1, |
|
506
|
|
|
|
|
|
|
user => $user, |
|
507
|
|
|
|
|
|
|
is_guest => 1, |
|
508
|
|
|
|
|
|
|
}; |
|
509
|
|
|
|
|
|
|
} |
|
510
|
|
|
|
|
|
|
|
|
511
|
1
|
|
|
1
|
1
|
16
|
sub login_guest($self, $user_input, $guest_user_key) { |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
2
|
|
|
512
|
|
|
|
|
|
|
# Convert guest to logged-in user, transferring session data (shopping cart, etc.) |
|
513
|
|
|
|
|
|
|
# $user_input: hashref with user_id, moniker, password (and optional email, etc.) |
|
514
|
|
|
|
|
|
|
# This creates a new user account and transfers the guest's session data to it. |
|
515
|
|
|
|
|
|
|
|
|
516
|
|
|
|
|
|
|
# Step 1: Lookup guest's session_id from user_key mapping |
|
517
|
1
|
|
|
|
|
3
|
my $guest_mapping = $self->{user_keys}{$guest_user_key}; |
|
518
|
1
|
50
|
|
|
|
5
|
return { success => 0, message => 'Guest user_key not found' } |
|
519
|
|
|
|
|
|
|
unless $guest_mapping; |
|
520
|
|
|
|
|
|
|
|
|
521
|
1
|
|
|
|
|
3
|
my $guest_session_id = $guest_mapping->{session_id}; |
|
522
|
|
|
|
|
|
|
|
|
523
|
|
|
|
|
|
|
# Step 2: Get guest session and extract its data |
|
524
|
1
|
|
|
|
|
6
|
my $session_result = $self->sessions->get_session($guest_session_id); |
|
525
|
|
|
|
|
|
|
return { success => 0, message => 'Guest session not found' } |
|
526
|
1
|
50
|
|
|
|
1268
|
unless $session_result->{success}; |
|
527
|
|
|
|
|
|
|
|
|
528
|
1
|
|
|
|
|
65
|
my $guest_session = $session_result->{session}; |
|
529
|
1
|
|
|
|
|
5
|
my $guest_data_result = $guest_session->get_data(); |
|
530
|
1
|
|
50
|
|
|
12
|
my $guest_data = $guest_data_result->{value} || {}; |
|
531
|
|
|
|
|
|
|
|
|
532
|
|
|
|
|
|
|
# Step 3: Create the new user account (Auth + Users) |
|
533
|
1
|
|
|
|
|
5
|
my $add_result = $self->add_user($user_input); |
|
534
|
1
|
50
|
|
|
|
8
|
return $add_result unless $add_result->{success}; |
|
535
|
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
# Step 4: Log in the newly created user (authenticate, create session, get User object) |
|
537
|
1
|
|
|
|
|
9
|
my $auth_data = $auth_data_filter->($user_input); |
|
538
|
|
|
|
|
|
|
my $login_result = $self->login_user({ |
|
539
|
|
|
|
|
|
|
user_id => $auth_data->{user_id}, |
|
540
|
|
|
|
|
|
|
password => $auth_data->{password}, |
|
541
|
1
|
|
|
|
|
45
|
}); |
|
542
|
1
|
50
|
|
|
|
11
|
return $login_result unless $login_result->{success}; |
|
543
|
|
|
|
|
|
|
|
|
544
|
1
|
|
|
|
|
3
|
my $user = $login_result->{user}; |
|
545
|
|
|
|
|
|
|
|
|
546
|
|
|
|
|
|
|
# Step 5: Transfer guest session data to new user's session |
|
547
|
1
|
50
|
|
|
|
30
|
if (%$guest_data) { |
|
548
|
1
|
|
|
|
|
13
|
my $new_session = $user->session(); |
|
549
|
1
|
50
|
|
|
|
6
|
if ($new_session) { |
|
550
|
1
|
|
|
|
|
9
|
$new_session->set_data($guest_data); |
|
551
|
1
|
|
|
|
|
19
|
$new_session->save(); |
|
552
|
|
|
|
|
|
|
} |
|
553
|
|
|
|
|
|
|
} |
|
554
|
|
|
|
|
|
|
|
|
555
|
|
|
|
|
|
|
# Step 6: Delete guest session |
|
556
|
1
|
|
|
|
|
28846
|
$self->sessions->delete_session($guest_session_id); |
|
557
|
|
|
|
|
|
|
|
|
558
|
|
|
|
|
|
|
# Step 7: Remove guest user_key mapping |
|
559
|
1
|
|
|
|
|
16211
|
delete $self->{user_keys}{$guest_user_key}; |
|
560
|
1
|
|
|
|
|
9
|
$self->save_user_keys(); |
|
561
|
|
|
|
|
|
|
|
|
562
|
|
|
|
|
|
|
return { |
|
563
|
1
|
|
|
|
|
27
|
success => 1, |
|
564
|
|
|
|
|
|
|
message => 'Guest converted to logged-in user', |
|
565
|
|
|
|
|
|
|
user => $user, |
|
566
|
|
|
|
|
|
|
}; |
|
567
|
|
|
|
|
|
|
} |
|
568
|
|
|
|
|
|
|
|
|
569
|
|
|
|
|
|
|
# Restore user: reconstruct User object from a user_key (e.g., from a cookie) |
|
570
|
4
|
|
|
4
|
1
|
19404
|
sub restore_user ($self, $user_key) { |
|
|
4
|
|
|
|
|
10
|
|
|
|
4
|
|
|
|
|
65
|
|
|
|
4
|
|
|
|
|
18
|
|
|
571
|
4
|
50
|
33
|
|
|
52
|
return { success => 0, message => 'user_key is required' } |
|
572
|
|
|
|
|
|
|
unless defined $user_key && length($user_key); |
|
573
|
|
|
|
|
|
|
|
|
574
|
|
|
|
|
|
|
# Step 1: Lookup user_key in mapping |
|
575
|
4
|
|
|
|
|
16
|
my $mapping = $self->{user_keys}{$user_key}; |
|
576
|
4
|
100
|
|
|
|
18
|
return { success => 0, message => 'user_key not found' } |
|
577
|
|
|
|
|
|
|
unless $mapping; |
|
578
|
|
|
|
|
|
|
|
|
579
|
3
|
|
|
|
|
8
|
my $user_id = $mapping->{user_id}; |
|
580
|
3
|
|
|
|
|
9
|
my $session_id = $mapping->{session_id}; |
|
581
|
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
# Step 2: Retrieve session (validates it still exists and hasn't expired) |
|
583
|
3
|
|
|
|
|
21
|
my $session_result = $self->sessions->get_session($session_id); |
|
584
|
3
|
100
|
|
|
|
3266
|
unless ($session_result->{success}) { |
|
585
|
|
|
|
|
|
|
# Session gone or expired -- clean up stale mapping |
|
586
|
1
|
|
|
|
|
5
|
delete $self->{user_keys}{$user_key}; |
|
587
|
1
|
|
|
|
|
6
|
$self->save_user_keys(); |
|
588
|
1
|
|
|
|
|
10
|
return { success => 0, message => 'Session expired' }; |
|
589
|
|
|
|
|
|
|
} |
|
590
|
|
|
|
|
|
|
|
|
591
|
2
|
|
|
|
|
5
|
my $session = $session_result->{session}; |
|
592
|
|
|
|
|
|
|
|
|
593
|
|
|
|
|
|
|
# Step 3: Determine user type -- logged-in or guest |
|
594
|
2
|
|
|
|
|
10
|
my $user_result = $self->users->get_user($user_id); |
|
595
|
|
|
|
|
|
|
|
|
596
|
2
|
100
|
|
|
|
647
|
if ($user_result->{success}) { |
|
597
|
|
|
|
|
|
|
# Logged-in user: rebuild with user data and backend closures |
|
598
|
1
|
|
|
|
|
5
|
my ($get, $update) = $self->_make_user_closures($user_id); |
|
599
|
|
|
|
|
|
|
my $user = Concierge::User->enable_user($user_id, { |
|
600
|
|
|
|
|
|
|
session => $session, |
|
601
|
|
|
|
|
|
|
user_data => $user_result->{user}, |
|
602
|
1
|
|
|
|
|
11
|
user_key => $user_key, |
|
603
|
|
|
|
|
|
|
_get_user_data => $get, |
|
604
|
|
|
|
|
|
|
_update_user_data => $update, |
|
605
|
|
|
|
|
|
|
}); |
|
606
|
|
|
|
|
|
|
|
|
607
|
|
|
|
|
|
|
return { |
|
608
|
1
|
|
|
|
|
9
|
success => 1, |
|
609
|
|
|
|
|
|
|
message => 'User restored', |
|
610
|
|
|
|
|
|
|
user => $user, |
|
611
|
|
|
|
|
|
|
}; |
|
612
|
|
|
|
|
|
|
} |
|
613
|
|
|
|
|
|
|
else { |
|
614
|
|
|
|
|
|
|
# Guest: session only, no user data |
|
615
|
1
|
|
|
|
|
30
|
my $user = Concierge::User->enable_user($user_id, { |
|
616
|
|
|
|
|
|
|
session => $session, |
|
617
|
|
|
|
|
|
|
user_key => $user_key, |
|
618
|
|
|
|
|
|
|
}); |
|
619
|
|
|
|
|
|
|
|
|
620
|
|
|
|
|
|
|
return { |
|
621
|
1
|
|
|
|
|
12
|
success => 1, |
|
622
|
|
|
|
|
|
|
message => 'Guest restored', |
|
623
|
|
|
|
|
|
|
user => $user, |
|
624
|
|
|
|
|
|
|
is_guest => 1, |
|
625
|
|
|
|
|
|
|
}; |
|
626
|
|
|
|
|
|
|
} |
|
627
|
|
|
|
|
|
|
} |
|
628
|
|
|
|
|
|
|
|
|
629
|
|
|
|
|
|
|
# Login user: authenticate, create session, assign user_key and store external_key mapping |
|
630
|
17
|
|
|
17
|
1
|
31525
|
sub login_user ($self, $credentials, $session_opts={}) { |
|
|
17
|
|
|
|
|
42
|
|
|
|
17
|
|
|
|
|
31
|
|
|
|
17
|
|
|
|
|
41
|
|
|
|
17
|
|
|
|
|
45
|
|
|
631
|
|
|
|
|
|
|
# Step 0: Get credentials |
|
632
|
17
|
|
|
|
|
122
|
my $auth_data = $auth_data_filter->($credentials); |
|
633
|
17
|
50
|
|
|
|
407
|
return { success => 0, message => 'Missing user_id or password' } |
|
634
|
|
|
|
|
|
|
unless $auth_data; |
|
635
|
|
|
|
|
|
|
|
|
636
|
17
|
|
|
|
|
66
|
my $user_id = $auth_data->{user_id}; |
|
637
|
17
|
|
|
|
|
40
|
my $password = $auth_data->{password}; |
|
638
|
|
|
|
|
|
|
|
|
639
|
|
|
|
|
|
|
# Step 1: Get user from database |
|
640
|
17
|
|
|
|
|
84
|
my $user_result = $self->users->get_user($user_id); |
|
641
|
|
|
|
|
|
|
return { success => 0, message => 'User not found' } |
|
642
|
17
|
50
|
|
|
|
7936
|
unless $user_result->{success}; |
|
643
|
|
|
|
|
|
|
|
|
644
|
|
|
|
|
|
|
# Step 2: Authenticate with ID & password |
|
645
|
17
|
|
|
|
|
83
|
my ($auth_ok, $auth_msg) = $self->auth->checkPwd($user_id, $password); |
|
646
|
17
|
50
|
0
|
|
|
22857913
|
return { success => 0, message => $auth_msg || 'Authentication failed' } |
|
647
|
|
|
|
|
|
|
unless $auth_ok; |
|
648
|
|
|
|
|
|
|
|
|
649
|
|
|
|
|
|
|
# Step 3: Create session |
|
650
|
|
|
|
|
|
|
my $session_result = $self->sessions->new_session( |
|
651
|
|
|
|
|
|
|
user_id => $user_id, |
|
652
|
17
|
50
|
|
|
|
189
|
%{ $session_opts || {} }, |
|
|
17
|
|
|
|
|
324
|
|
|
653
|
|
|
|
|
|
|
); |
|
654
|
|
|
|
|
|
|
return { success => 0, message => $session_result->{message} || 'Failed to create session' } |
|
655
|
17
|
50
|
0
|
|
|
638646
|
unless $session_result->{success}; |
|
656
|
|
|
|
|
|
|
|
|
657
|
17
|
|
|
|
|
47
|
my $session = $session_result->{session}; |
|
658
|
17
|
|
|
|
|
95
|
my $session_id = $session->session_id(); |
|
659
|
|
|
|
|
|
|
|
|
660
|
|
|
|
|
|
|
# Create user object for logged-in user |
|
661
|
17
|
|
|
|
|
340
|
my ($get, $update) = $self->_make_user_closures($user_id); |
|
662
|
|
|
|
|
|
|
my $user = Concierge::User->enable_user($user_id, { |
|
663
|
|
|
|
|
|
|
session => $session, |
|
664
|
|
|
|
|
|
|
user_data => $user_result->{user}, |
|
665
|
17
|
|
|
|
|
388
|
_get_user_data => $get, |
|
666
|
|
|
|
|
|
|
_update_user_data => $update, |
|
667
|
|
|
|
|
|
|
}); |
|
668
|
|
|
|
|
|
|
|
|
669
|
|
|
|
|
|
|
# Store user_key mapping |
|
670
|
17
|
|
|
|
|
158
|
$self->{user_keys}{$user->user_key()} = { |
|
671
|
|
|
|
|
|
|
user_id => $user_id, |
|
672
|
|
|
|
|
|
|
session_id => $session_id, |
|
673
|
|
|
|
|
|
|
}; |
|
674
|
17
|
|
|
|
|
110
|
$self->save_user_keys(); |
|
675
|
|
|
|
|
|
|
|
|
676
|
|
|
|
|
|
|
return { |
|
677
|
17
|
|
|
|
|
896
|
success => 1, |
|
678
|
|
|
|
|
|
|
message => 'Login successful', |
|
679
|
|
|
|
|
|
|
user => $user, |
|
680
|
|
|
|
|
|
|
}; |
|
681
|
|
|
|
|
|
|
} |
|
682
|
|
|
|
|
|
|
|
|
683
|
|
|
|
|
|
|
# Verify password: check if password is correct for user |
|
684
|
4
|
|
|
4
|
1
|
4993
|
sub verify_password ($self, $user_id, $password) { |
|
|
4
|
|
|
|
|
9
|
|
|
|
4
|
|
|
|
|
10
|
|
|
|
4
|
|
|
|
|
6
|
|
|
|
4
|
|
|
|
|
6
|
|
|
685
|
|
|
|
|
|
|
# Verifies if provided password is correct for the user |
|
686
|
|
|
|
|
|
|
# Usually not needed - if user has valid session/user_key, they're already authenticated |
|
687
|
|
|
|
|
|
|
# Use cases: sensitive operations requiring re-authentication, admin verification, etc. |
|
688
|
|
|
|
|
|
|
# Most password resets don't need this - session authentication is sufficient |
|
689
|
|
|
|
|
|
|
|
|
690
|
|
|
|
|
|
|
# Minimal validation - application controls when this is called |
|
691
|
4
|
50
|
33
|
|
|
30
|
return { success => 0, message => 'user_id is required' } |
|
692
|
|
|
|
|
|
|
unless defined $user_id && length($user_id); |
|
693
|
|
|
|
|
|
|
|
|
694
|
4
|
50
|
33
|
|
|
24
|
return { success => 0, message => 'password is required' } |
|
695
|
|
|
|
|
|
|
unless defined $password && length($password); |
|
696
|
|
|
|
|
|
|
|
|
697
|
|
|
|
|
|
|
# Check password via Auth component |
|
698
|
4
|
|
|
|
|
20
|
my ($pwd_ok, $pwd_msg) = $self->auth->checkPwd($user_id, $password); |
|
699
|
|
|
|
|
|
|
|
|
700
|
|
|
|
|
|
|
return { |
|
701
|
4
|
100
|
33
|
|
|
5431259
|
success => $pwd_ok ? 1 : 0, |
|
702
|
|
|
|
|
|
|
message => $pwd_msg || ($pwd_ok ? 'Password verified' : 'Invalid password'), |
|
703
|
|
|
|
|
|
|
user_id => $user_id, |
|
704
|
|
|
|
|
|
|
}; |
|
705
|
|
|
|
|
|
|
} |
|
706
|
|
|
|
|
|
|
|
|
707
|
|
|
|
|
|
|
# Reset password: set user's new password |
|
708
|
1
|
|
|
1
|
1
|
3159
|
sub reset_password ($self, $user_id, $new_password) { |
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
1
|
|
|
709
|
|
|
|
|
|
|
# Changes user password using Auth component |
|
710
|
|
|
|
|
|
|
# Application is responsible for verifying user identity and old password if needed |
|
711
|
|
|
|
|
|
|
|
|
712
|
1
|
50
|
33
|
|
|
9
|
return { success => 0, message => 'user_id is required' } |
|
713
|
|
|
|
|
|
|
unless defined $user_id && length($user_id); |
|
714
|
|
|
|
|
|
|
|
|
715
|
1
|
50
|
33
|
|
|
17
|
return { success => 0, message => 'new_password is required' } |
|
716
|
|
|
|
|
|
|
unless defined $new_password && length($new_password); |
|
717
|
|
|
|
|
|
|
|
|
718
|
|
|
|
|
|
|
# Reset existing password using Auth's resetPwd |
|
719
|
|
|
|
|
|
|
# Pass through Auth's messages (Auth provides detailed error messages) |
|
720
|
1
|
|
|
|
|
6
|
my ($reset_ok, $reset_msg) = $self->auth->resetPwd($user_id, $new_password); |
|
721
|
|
|
|
|
|
|
|
|
722
|
|
|
|
|
|
|
return { |
|
723
|
1
|
50
|
33
|
|
|
1533670
|
success => $reset_ok ? 1 : 0, |
|
724
|
|
|
|
|
|
|
message => $reset_msg || ($reset_ok ? 'Password reset successful' : 'Password reset failed'), |
|
725
|
|
|
|
|
|
|
user_id => $user_id, |
|
726
|
|
|
|
|
|
|
}; |
|
727
|
|
|
|
|
|
|
} |
|
728
|
|
|
|
|
|
|
|
|
729
|
|
|
|
|
|
|
# Logout user: delete session and remove from concierge mapping |
|
730
|
1
|
|
|
1
|
1
|
1934
|
sub logout_user ($self, $session_id) { |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
1
|
|
|
731
|
|
|
|
|
|
|
# Logs out user by deleting their session |
|
732
|
|
|
|
|
|
|
# Removes from concierge user_keys mapping |
|
733
|
|
|
|
|
|
|
|
|
734
|
1
|
50
|
33
|
|
|
18
|
return { success => 0, message => 'No session provided for logout' } |
|
735
|
|
|
|
|
|
|
unless defined $session_id && length($session_id); |
|
736
|
|
|
|
|
|
|
|
|
737
|
|
|
|
|
|
|
# Step 1: Verify session exists |
|
738
|
1
|
|
|
|
|
5
|
my $session_check = $self->sessions->get_session($session_id); |
|
739
|
1
|
50
|
|
|
|
1069
|
unless ($session_check->{success}) { |
|
740
|
0
|
|
|
|
|
0
|
return { success => 0, message => 'Session not found for logout' }; |
|
741
|
|
|
|
|
|
|
} |
|
742
|
|
|
|
|
|
|
|
|
743
|
|
|
|
|
|
|
# Step 2: Find user_key for this session (to remove from mapping) |
|
744
|
1
|
|
|
|
|
3
|
my $user_key_to_delete; |
|
745
|
|
|
|
|
|
|
my $user_id; |
|
746
|
|
|
|
|
|
|
|
|
747
|
1
|
|
|
|
|
2
|
for my $key (keys %{$self->{user_keys}}) { |
|
|
1
|
|
|
|
|
5
|
|
|
748
|
1
|
50
|
|
|
|
6
|
if ($self->{user_keys}{$key}{session_id} eq $session_id) { |
|
749
|
1
|
|
|
|
|
3
|
$user_key_to_delete = $key; |
|
750
|
1
|
|
|
|
|
3
|
$user_id = $self->{user_keys}{$key}{user_id}; |
|
751
|
1
|
|
|
|
|
2
|
last; |
|
752
|
|
|
|
|
|
|
} |
|
753
|
|
|
|
|
|
|
} |
|
754
|
|
|
|
|
|
|
|
|
755
|
|
|
|
|
|
|
# Step 3: Delete session from Sessions component |
|
756
|
1
|
|
|
|
|
5
|
my $delete_result = $self->sessions->delete_session($session_id); |
|
757
|
1
|
50
|
|
|
|
17019
|
unless ($delete_result->{success}) { |
|
758
|
0
|
|
0
|
|
|
0
|
return { success => 0, message => $delete_result->{message} || 'Failed to delete session' }; |
|
759
|
|
|
|
|
|
|
} |
|
760
|
|
|
|
|
|
|
|
|
761
|
|
|
|
|
|
|
# Step 4: Remove from concierge user_keys mapping |
|
762
|
1
|
50
|
|
|
|
5
|
if ($user_key_to_delete) { |
|
763
|
1
|
|
|
|
|
9
|
delete $self->{user_keys}{$user_key_to_delete}; |
|
764
|
1
|
|
|
|
|
33
|
my $save_result = $self->save_user_keys(); |
|
765
|
1
|
50
|
|
|
|
25
|
unless ($save_result->{success}) { |
|
766
|
0
|
|
|
|
|
0
|
return { success => 0, message => 'Session deleted but failed to update mapping' }; |
|
767
|
|
|
|
|
|
|
} |
|
768
|
|
|
|
|
|
|
} |
|
769
|
|
|
|
|
|
|
|
|
770
|
|
|
|
|
|
|
return { |
|
771
|
1
|
50
|
|
|
|
22
|
success => 1, |
|
772
|
|
|
|
|
|
|
message => 'Logout successful', |
|
773
|
|
|
|
|
|
|
session_id => $session_id, |
|
774
|
|
|
|
|
|
|
($user_id ? (user_id => $user_id) : ()), |
|
775
|
|
|
|
|
|
|
}; |
|
776
|
|
|
|
|
|
|
} |
|
777
|
|
|
|
|
|
|
|
|
778
|
|
|
|
|
|
|
|
|
779
|
|
|
|
|
|
|
1; |
|
780
|
|
|
|
|
|
|
|
|
781
|
|
|
|
|
|
|
__END__ |