User - viames/pair GitHub Wiki
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
usernameoremaildepending onPAIR_AUTH_BY_EMAIL - rejects disabled users and users with more than 9 failed attempts
- verifies the password with
checkPassword() - creates a
Session, updateslastLogin, resetsfaults, clearspwReset, and writes audit logs - returns an object with
error,message,userId, andsessionId
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) {
return ApiResponse::errorResponse('UNAUTHORIZED');
}
return ApiResponse::jsonResponse([
'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
usermodule is always allowed publicis always allowed- the method accepts either
module+actionor a singlemodule/actionstring - 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
Applicationduring 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): ?UsersetNewPassword(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 passwordgetHashedPasswordWithSalt($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(): ?staticreturns theApplicationcurrent user ornull.avatar(string $classPrefix = 'user'): stringrenders initials with a deterministic template-based color.fullName(): stringreturns"name surname".- Virtual properties
fullNameandgroupNameare available through__get(). getGroup(): Grouploads the related group.getLocale(): Localereturns the stored locale or the default locale.getLanguageCode(): ?stringreturns the cached language code derived from the locale relation.getDateTimeZone(): DateTimeZonereads the current session timezone or falls back toBASE_TIMEZONE.getValidTimeZone(string $timezone): DateTimeZonevalidates an IANA timezone name.isSuper(): boolchecks both the current and former impersonating user.isDeletable(): boolrefuses self-deletion and then applies the normal ActiveRecord FK checks.isLocaleSet(): booltells whetherlocaleIdis 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=trueswitches local login lookup fromusernametoemail.PAIR_SINGLE_SESSION=truedeletes 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.