Concierge

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN

authenticated session.

Between requests, users are restored by `user_key` (typically stored in a
cookie): `restore_user($user_key)` rehydrates the correct object type with
the right data and backend access.

## Component Capabilities

### Authentication — Concierge::Auth

- **Argon2id** password hashing and verification; no plaintext credentials
  written to disk
- Random value generators: hex IDs, alphanumeric tokens, UUIDs (v4),
  word-passphrases from a system dictionary
- Designed for substitution: swap in any replacement that implements the
  same method contract (`checkPwd`, `setPwd`, `resetPwd`, `deleteID`, etc.)
  for LDAP, OAuth, or other schemes

### Sessions — Concierge::Sessions

- **Multiple backends**: SQLite (recommended), flat-file, or in-memory text

lib/Concierge.pm  view on Meta::CPAN


# === COMPONENT MODULES ===
use Concierge::Auth;
use Concierge::Sessions;
use Concierge::Users;
use Concierge::Desk::User;

# === PARAMETER FILTERS ===
# Shared filters for secure data segregation

# Auth filter - ONLY credentials (user_id + password)
our $auth_data_filter = make_filter(
    [qw(user_id password)],                   # required credentials
    [],                                        # accepted - nothing else
    [],                                        # excluded - not needed
);

# User data filter - everything EXCEPT credentials
# Handles both minimal input (user_id, moniker) and
# rich input (user_id, moniker, email, phone, bio, etc.)
our $user_data_filter = make_filter(
    [qw(user_id moniker)],                    # required minimum
    ['*'],                                    # accept ALL other fields, except:
    [qw(password confirm_password)],          # excluded - security boundary
);

# Session data filter - for populating session with initial data
# Accepts user_id (required for new_session) plus any session fields
# Excludes credentials (never stored in session data)
our $session_data_filter = make_filter(
    [qw(user_id)],                            # required for new_session
    ['*'],                                    # accept all other fields, except:
    [qw(password confirm_password)],          # excluded - security boundary
);

# User update filter - for updating existing user records
# No required fields (user_id passed separately as parameter)
# Excludes user_id (identity field), password (use reset_password instead)
our $user_update_filter = make_filter(

lib/Concierge.pm  view on Meta::CPAN

        return {
            success  => 1,
            message  => 'Guest restored',
            user     => $user,
            is_guest => 1,
        };
    }
}

# Login user: authenticate, create session, assign user_key and store external_key mapping
sub login_user ($self, $credentials, $session_opts={}) {
    # Step 0: Get credentials
    my $auth_data = $auth_data_filter->($credentials);
    return { success => 0, message => 'Missing user_id or password' }
        unless $auth_data;

    my $user_id = $auth_data->{user_id};
    my $password = $auth_data->{password};

    # Step 1: Get user from database
    my $user_result = $self->users->get_user($user_id);
    return { success => 0, message => 'User not found' }
        unless $user_result->{success};

lib/Concierge.pm  view on Meta::CPAN

Applications interact only with Concierge and the L<Concierge::Desk::User> objects
it returns. The component modules are never exposed directly.

=head2 What the Suite Provides

Concierge handles orchestration -- coordinating components, managing the
user_key mapping, and returning consistent structured results. The
capabilities of the suite live in the three components:

B<Authentication> (L<Concierge::Auth>): Argon2id password hashing and
verification; no plaintext credentials are ever written to disk. Also
provides random token, UUID, word-passphrase, and hex-ID generators. The
component is substitutable: any replacement implementing the same method
contract (C<checkPwd>, C<setPwd>, C<resetPwd>, etc.) can replace it for
LDAP, OAuth, or any other scheme.

B<Sessions> (L<Concierge::Sessions>): Full session lifecycle -- creation,
retrieval, expiry, and cleanup -- with SQLite, file, or in-memory backends.
Sessions carry arbitrary key/value data. A single-session-per-user policy
is enforced: creating a new session automatically removes any prior session
for that user. Expired sessions are cleaned up each time a desk is opened.

lib/Concierge.pm  view on Meta::CPAN

Assigned a unique identifier only. No session, no stored data. Suitable for
anonymous tracking (e.g., cookies).

=item B<Guest> -- C<checkin_guest()>

Assigned an identifier and a session. Can store temporary data (e.g., a
shopping cart). No authentication or persistent user record.

=item B<Logged-in user> -- C<login_user()>

Authenticated with credentials. Has a session, persistent user data, and
full access to the User object's data methods.

=back

A guest can be converted to a logged-in user with C<login_guest()>,
transferring any session data accumulated during the guest session.

=head2 User Keys

Each active user (guest or logged-in) is tracked by a I<user_key> -- a

lib/Concierge.pm  view on Meta::CPAN


    my $result = $concierge->checkin_guest(\%session_opts);
    my $user = $result->{user};    # Concierge::Desk::User (guest)

Creates a guest with a generated identifier and a session. The optional
C<%session_opts> hashref may include C<timeout> (in seconds; defaults to
1800).

=head3 login_user

    my $result = $concierge->login_user(\%credentials, \%session_opts);
    my $user = $result->{user};    # Concierge::Desk::User (logged-in)

Authenticates C<user_id> and C<password> from C<%credentials>, retrieves
the user's data record, creates a session, and returns a fully-equipped
User object. If the user already has an active session, the previous
session is replaced.

=head3 restore_user

    my $result = $concierge->restore_user($user_key);
    my $user = $result->{user};    # Concierge::Desk::User (guest or logged-in)

Reconstructs a User object from a C<user_key> (typically stored in a cookie

lib/Concierge.pm  view on Meta::CPAN


If the session has expired, the stale mapping entry is cleaned up and the
method returns failure. The application can then redirect to login or create
a new guest as appropriate.

Returns C<< { success => 1, user => $user } >> on success. Guest restores
also include C<< is_guest => 1 >>.

=head3 login_guest

    my $result = $concierge->login_guest(\%credentials, $guest_user_key);
    my $user = $result->{user};    # Concierge::Desk::User (logged-in)

Converts a guest to a logged-in user. Authenticates with C<%credentials>,
transfers any data from the guest's session to the new session, then
deletes the guest session and removes the guest's user_key mapping.

=head3 logout_user

    my $result = $concierge->logout_user($session_id);

Deletes the session and removes the user_key mapping entry.

=head2 Admin Operations

lib/Concierge.pm  view on Meta::CPAN

=item C<$auth_data_filter> -- extracts only C<user_id> and C<password>

=item C<$user_data_filter> -- extracts everything except C<password>

=item C<$session_data_filter> -- extracts C<user_id> plus non-credential fields

=item C<$user_update_filter> -- excludes C<user_id> and C<password> from updates

=back

These ensure that credentials never leak into user data stores and that
identity fields cannot be changed via update operations.

=head1 EXTENSIBILITY

=head2 Component Substitution

Each identity core component can be replaced with a drop-in alternative as
long as the replacement implements the methods Concierge calls on it.

B<Auth> -- Concierge calls:

t/04-user-operations.t  view on Meta::CPAN


subtest 'list_users with include_data' => sub {
    my $result = $concierge->list_users('', { include_data => 1 });

    ok $result->{success}, 'list_users with data succeeds';
    ref_ok $result->{users}, 'HASH', 'users is hash';
    ok exists $result->{users}{alice}, 'alice data included';
    is $result->{users}{alice}{moniker}, 'Alice', 'alice data correct';
};

subtest 'verify_password checks credentials' => sub {
    my $result = $concierge->verify_password('alice', 'secret123');
    ok $result->{success}, 'correct password verified';

    $result = $concierge->verify_password('alice', 'wrongpass');
    ok !$result->{success}, 'incorrect password rejected';
};

subtest 'reset_password changes password' => sub {
    my $result = $concierge->reset_password('alice', 'newsecret456');
    ok $result->{success}, 'reset_password succeeds';



( run in 0.935 second using v1.01-cache-2.11-cpan-22024b96cdf )