File Coverage

lib/Concierge.pm
Criterion Covered Total %
statement 336 353 95.1
branch 87 148 58.7
condition 26 82 31.7
subroutine 32 32 100.0
pod 15 20 75.0
total 496 635 78.1


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__