Skip to content
Viames Marino edited this page Mar 26, 2026 · 3 revisions

Pair framework: User

Pair\Models\User is Pair's central authentication model. It extends ActiveRecord and coordinates:

  • local login with password verification
  • login by external factor (doLoginById()), useful for Passkey or SSO flows
  • ACL checks against modules and actions
  • session creation and logout
  • remember-me cookies and token rotation
  • locale, timezone, landing-page, and impersonation helpers

When to use

Use User whenever backend code needs authenticated identity, access checks, session bootstrap, password-reset completion, or remember-me behavior.

Main methods (deep dive)

1) doLogin(string $username, string $password, string $timezone): stdClass

This is the main entry point for local username or email login.

Current behavior:

  • loads the user by username or email depending on PAIR_AUTH_BY_EMAIL
  • rejects disabled users and users with more than 9 failed attempts
  • verifies the password with checkPassword()
  • creates a Session, updates lastLogin, resets faults, clears pwReset, and writes audit logs
  • returns an object with error, message, userId, and sessionId

Typical controller usage:

use App\Models\User;

$timezone = (string)($_POST['timezone'] ?? 'Europe/Rome');

$result = User::doLogin(
    trim((string)($_POST['username'] ?? '')),
    (string)($_POST['password'] ?? ''),
    $timezone
);

if ($result->error) {
    $this->toastError('Login failed', (string)$result->message);
    return;
}

$user = new User((int)$result->userId);

if (!empty($_POST['remember'])) {
    $user->createRememberMe($timezone);
}

$this->redirect('dashboard');

2) doLoginById(int $userId, string $timezone): stdClass

Use this when the identity has already been verified by another factor, for example Passkey/WebAuthn, OAuth callback handling, or a trusted SSO flow.

It follows the same safety rules as doLogin(): locked or disabled users are still rejected, and a normal Pair session is created.

Example from an API or Passkey flow:

use App\Models\User;
use Pair\Api\ApiResponse;

$result = User::doLoginById($verifiedUserId, 'Europe/Rome');

if ($result->error) {
    ApiResponse::error('UNAUTHORIZED');
}

ApiResponse::respond([
    'userId' => $result->userId,
    'sid' => $result->sessionId,
]);

3) doLogout(string $sid): bool

This closes the session, removes persistent state cookies, unsets remember-me data, resets Application::currentUser, and writes the logout audit entry.

use App\Models\User;

$ok = User::doLogout(session_id());

if ($ok) {
    $this->redirect('user/login');
}

4) canAccess(string $module, ?string $action = null): bool

This is the main ACL check used by Pair.

Current behavior:

  • super users always pass
  • the user module is always allowed
  • public is always allowed
  • the method accepts either module + action or a single module/action string
  • custom routes are resolved before ACL matching
  • rules are loaded once and cached on the user object

Examples:

if (!$user->canAccess('orders', 'edit')) {
    throw new \RuntimeException('Access denied');
}
if ($user->canAccess('reports/export')) {
    // module/action combined in one string
}

5) Remember-me lifecycle: createRememberMe(), loginByRememberMe(), renewRememberMe(), unsetRememberMe()

These methods implement the persistent login flow.

createRememberMe():

  • generates a random token
  • stores only the hashed token in users_remembers
  • writes a versioned cookie payload
  • keeps only one active remember-me token per user

loginByRememberMe():

  • is used automatically by Application during unauthenticated web requests
  • validates the cookie, loads the related user, creates a fresh session, rotates the remember-me token, and sets the current application user

renewRememberMe() rotates the cookie and DB token pair.

unsetRememberMe() removes both cookie and server-side token.

Example after a successful login:

$result = \App\Models\User::doLogin($username, $password, $timezone);

if (!$result->error && !empty($_POST['remember'])) {
    $user = new \App\Models\User((int)$result->userId);
    $user->createRememberMe($timezone);
}

Example manual auto-login check in a custom bootstrap path:

if (\App\Models\User::loginByRememberMe()) {
    \Pair\Core\Application::getInstance()->redirectToUserDefault();
}

6) Landing helpers: landing() and redirectToDefault()

landing() returns the default module/action for the user's ACL group. redirectToDefault() turns that into a browser redirect.

$landing = $user->landing();

if ($landing) {
    echo $landing->module . '/' . $landing->action;
}
$user->redirectToDefault();

7) Password reset and password helpers

The main reset path is:

  • getByPwReset(string $token): ?User
  • setNewPassword(string $newPassword, string $timezone): bool

setNewPassword() clears the reset token, stores the new hash, creates a new session, resets faults, and writes the audit event.

use App\Models\User;

$user = User::getByPwReset((string)($_GET['token'] ?? ''));

if (!$user) {
    throw new \RuntimeException('Invalid reset token');
}

$user->setNewPassword((string)$_POST['password'], 'Europe/Rome');

The low-level helpers are also useful:

  • checkPassword($plain, $hash) verifies a local password
  • getHashedPasswordWithSalt($plain) builds the stored hash

8) Impersonation: impersonate(), impersonateStop(), isSuper()

impersonate() swaps the active session user while remembering the former user ID. impersonateStop() restores the original user. isSuper() also checks the former user during impersonation so elevated access is preserved correctly.

$admin->impersonate($targetUser);
$currentUser->impersonateStop();

Secondary methods (short reference)

  • current(): ?static returns the Application current user or null.
  • avatar(string $classPrefix = 'user'): string renders initials with a deterministic template-based color.
  • fullName(): string returns "name surname".
  • Virtual properties fullName and groupName are available through __get().
  • getGroup(): Group loads the related group.
  • getLocale(): Locale returns the stored locale or the default locale.
  • getLanguageCode(): ?string returns the cached language code derived from the locale relation.
  • getDateTimeZone(): DateTimeZone reads the current session timezone or falls back to BASE_TIMEZONE.
  • getValidTimeZone(string $timezone): DateTimeZone validates an IANA timezone name.
  • isSuper(): bool checks both the current and former impersonating user.
  • isDeletable(): bool refuses self-deletion and then applies the normal ActiveRecord FK checks.
  • isLocaleSet(): bool tells whether localeId is currently set.

Hooks you can override

Authentication hooks:

  • beforeLogin(), afterLogin()
  • afterLoginFailed()
  • beforeLogout(), afterLogout()

Remember-me hooks:

  • beforeRememberMeCreate(), afterRememberMeCreate()
  • beforeRememberMeLogin(), afterRememberMeLogin()
  • beforeRememberMeRenew(), afterRememberMeRenew()
  • beforeRememberMeUnset(), afterRememberMeUnset()

These are useful when you need application-specific audit, telemetry, or side effects without rewriting the core flow.

Practical notes

  • PAIR_AUTH_BY_EMAIL=true switches local login lookup from username to email.
  • PAIR_SINGLE_SESSION=true deletes the user's other sessions after a successful login.
  • The current implementation treats users with more than 9 faults as locked for login purposes.
  • Remember-me cookies store a versioned payload, while the DB keeps only a deterministic hash of the token.

See also: Session, Rule, Locale, UserRemember, OAuth2Token, PasskeyAuth.

Clone this wiki locally