Made login attempts functions OOP.
This commit is contained in:
parent
efee8cb67b
commit
174d25d40f
11 changed files with 196 additions and 106 deletions
21
database/2020_05_25_133726_login_attempts_table_fixes.php
Normal file
21
database/2020_05_25_133726_login_attempts_table_fixes.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
namespace Misuzu\DatabaseMigrations\LoginAttemptsTableFixes;
|
||||
|
||||
use PDO;
|
||||
|
||||
function migrate_up(PDO $conn): void {
|
||||
$conn->exec("
|
||||
ALTER TABLE `msz_login_attempts`
|
||||
DROP COLUMN `attempt_id`,
|
||||
ADD INDEX `login_attempts_created_index` (`attempt_created`);
|
||||
");
|
||||
}
|
||||
|
||||
function migrate_down(PDO $conn): void {
|
||||
$conn->exec("
|
||||
ALTER TABLE `msz_login_attempts`
|
||||
ADD COLUMN `attempt_id` INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,
|
||||
DROP INDEX `login_attempts_created_index`,
|
||||
ADD PRIMARY KEY (`attempt_id`);
|
||||
");
|
||||
}
|
|
@ -77,7 +77,6 @@ require_once 'src/Forum/validate.php';
|
|||
require_once 'src/Users/auth.php';
|
||||
require_once 'src/Users/avatar.php';
|
||||
require_once 'src/Users/background.php';
|
||||
require_once 'src/Users/login_attempt.php';
|
||||
require_once 'src/Users/recovery.php';
|
||||
require_once 'src/Users/relations.php';
|
||||
require_once 'src/Users/role.php';
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Misuzu;
|
|||
use Misuzu\Net\IPAddress;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserNotFoundException;
|
||||
use Misuzu\Users\UserLoginAttempt;
|
||||
|
||||
require_once '../../misuzu.php';
|
||||
|
||||
|
@ -23,7 +24,7 @@ $siteIsPrivate = Config::get('private.enable', Config::TYPE_BOOL);
|
|||
$loginPermCat = $siteIsPrivate ? Config::get('private.perm.cat', Config::TYPE_STR) : '';
|
||||
$loginPermVal = $siteIsPrivate ? Config::get('private.perm.val', Config::TYPE_INT) : 0;
|
||||
$ipAddress = IPAddress::remote();
|
||||
$remainingAttempts = user_login_attempts_remaining($ipAddress);
|
||||
$remainingAttempts = UserLoginAttempt::remaining();
|
||||
|
||||
while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
||||
if(!CSRF::validateRequest()) {
|
||||
|
@ -55,7 +56,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
try {
|
||||
$userData = User::findForLogin($_POST['login']['username']);
|
||||
} catch(UserNotFoundException $ex) {
|
||||
user_login_attempt_record(false, null, $ipAddress, $userAgent);
|
||||
UserLoginAttempt::create(false);
|
||||
$notices[] = $loginFailedError;
|
||||
break;
|
||||
}
|
||||
|
@ -66,7 +67,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
}
|
||||
|
||||
if($userData->isDeleted() || !$userData->checkPassword($_POST['login']['password'])) {
|
||||
user_login_attempt_record(false, $userData->getId(), $ipAddress, $userAgent);
|
||||
UserLoginAttempt::create(false, $userData);
|
||||
$notices[] = $loginFailedError;
|
||||
break;
|
||||
}
|
||||
|
@ -77,7 +78,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
|
||||
if(!empty($loginPermCat) && $loginPermVal > 0 && !perms_check_user($loginPermCat, $userData->getId(), $loginPermVal)) {
|
||||
$notices[] = "Login succeeded, but you're not allowed to browse the site right now.";
|
||||
user_login_attempt_record(true, $userData->getId(), $ipAddress, $userAgent);
|
||||
UserLoginAttempt::create(true, $userData);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -88,7 +89,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
|
|||
return;
|
||||
}
|
||||
|
||||
user_login_attempt_record(true, $userData->getId(), $ipAddress, $userAgent);
|
||||
UserLoginAttempt::create(true, $userData);
|
||||
$sessionKey = user_session_create($userData->getId(), $ipAddress, $userAgent);
|
||||
|
||||
if(empty($sessionKey)) {
|
||||
|
|
|
@ -5,6 +5,7 @@ use UnexpectedValueException;
|
|||
use Misuzu\AuditLog;
|
||||
use Misuzu\Net\IPAddress;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserLoginAttempt;
|
||||
|
||||
require_once '../../misuzu.php';
|
||||
|
||||
|
@ -29,7 +30,7 @@ $notices = [];
|
|||
$siteIsPrivate = Config::get('private.enable', Config::TYPE_BOOL);
|
||||
$canResetPassword = $siteIsPrivate ? Config::get('private.allow_password_reset', Config::TYPE_BOOL, true) : true;
|
||||
$ipAddress = IPAddress::remote();
|
||||
$remainingAttempts = user_login_attempts_remaining($ipAddress);
|
||||
$remainingAttempts = UserLoginAttempt::remaining();
|
||||
|
||||
while($canResetPassword) {
|
||||
if(!empty($reset) && $userId > 0) {
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Misuzu;
|
|||
use Misuzu\Net\IPAddress;
|
||||
use Misuzu\Net\IPAddressBlacklist;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserLoginAttempt;
|
||||
|
||||
require_once '../../misuzu.php';
|
||||
|
||||
|
@ -15,7 +16,7 @@ if(user_session_active()) {
|
|||
$register = !empty($_POST['register']) && is_array($_POST['register']) ? $_POST['register'] : [];
|
||||
$notices = [];
|
||||
$ipAddress = IPAddress::remote();
|
||||
$remainingAttempts = user_login_attempts_remaining($ipAddress);
|
||||
$remainingAttempts = UserLoginAttempt::remaining();
|
||||
$restricted = IPAddressBlacklist::check($ipAddress) ? 'blacklist'
|
||||
: (user_warning_check_ip($ipAddress) ? 'ban' : '');
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
namespace Misuzu;
|
||||
|
||||
use Misuzu\Net\IPAddress;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserLoginAttempt;
|
||||
|
||||
require_once '../../misuzu.php';
|
||||
|
||||
|
@ -13,16 +15,18 @@ if(user_session_active()) {
|
|||
$twofactor = !empty($_POST['twofactor']) && is_array($_POST['twofactor']) ? $_POST['twofactor'] : [];
|
||||
$notices = [];
|
||||
$ipAddress = IPAddress::remote();
|
||||
$remainingAttempts = user_login_attempts_remaining($ipAddress);
|
||||
$remainingAttempts = UserLoginAttempt::remaining();
|
||||
$tokenInfo = user_auth_tfa_token_info(
|
||||
!empty($_GET['token']) && is_string($_GET['token']) ? $_GET['token'] : (
|
||||
!empty($twofactor['token']) && is_string($twofactor['token']) ? $twofactor['token'] : ''
|
||||
)
|
||||
);
|
||||
|
||||
$userData = User::byId($tokenInfo['user_id']);
|
||||
|
||||
// checking user_totp_key specifically because there's a fringe chance that
|
||||
// there's a token present, but totp is actually disabled
|
||||
if(empty($tokenInfo['user_totp_key'])) {
|
||||
if(!$userData->hasTOTP()) {
|
||||
url_redirect('auth-login');
|
||||
return;
|
||||
}
|
||||
|
@ -46,24 +50,17 @@ while(!empty($twofactor)) {
|
|||
break;
|
||||
}
|
||||
|
||||
$totp = new TOTP($tokenInfo['user_totp_key']);
|
||||
$accepted = [
|
||||
$totp->generate(time()),
|
||||
$totp->generate(time() - 30),
|
||||
$totp->generate(time() + 30),
|
||||
];
|
||||
|
||||
if(!in_array($twofactor['code'], $accepted)) {
|
||||
if(!in_array($twofactor['code'], $userData->getValidTOTPTokens())) {
|
||||
$notices[] = sprintf(
|
||||
"Invalid two factor code, %d attempt%s remaining",
|
||||
$remainingAttempts - 1,
|
||||
$remainingAttempts === 2 ? '' : 's'
|
||||
);
|
||||
user_login_attempt_record(false, $tokenInfo['user_id'], $ipAddress, $userAgent);
|
||||
UserLoginAttempt::create(false, $userData);
|
||||
break;
|
||||
}
|
||||
|
||||
user_login_attempt_record(true, $tokenInfo['user_id'], $ipAddress, $userAgent);
|
||||
UserLoginAttempt::create(true, $userData);
|
||||
$sessionKey = user_session_create($tokenInfo['user_id'], $ipAddress, $userAgent);
|
||||
|
||||
if(empty($sessionKey)) {
|
||||
|
|
|
@ -4,26 +4,22 @@ namespace Misuzu;
|
|||
use Misuzu\AuditLog;
|
||||
use Misuzu\Pagination;
|
||||
use Misuzu\Users\User;
|
||||
use Misuzu\Users\UserLoginAttempt;
|
||||
|
||||
require_once '../../misuzu.php';
|
||||
|
||||
if(!user_session_active()) {
|
||||
$currentUser = User::getCurrent();
|
||||
|
||||
if($currentUser === null) {
|
||||
echo render_error(401);
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUser = User::getCurrent();
|
||||
$loginHistoryPagination = new Pagination(user_login_attempts_count($currentUser->getId()), 15, 'hp');
|
||||
$loginHistoryPagination = new Pagination(UserLoginAttempt::countAll($currentUser), 15, 'hp');
|
||||
$accountLogPagination = new Pagination(AuditLog::countAll($currentUser), 15, 'ap');
|
||||
|
||||
$loginHistoryList = user_login_attempts_list(
|
||||
$loginHistoryPagination->getOffset(),
|
||||
$loginHistoryPagination->getRange(),
|
||||
$currentUser->getId()
|
||||
);
|
||||
|
||||
Template::render('settings.logs', [
|
||||
'login_history_list' => $loginHistoryList,
|
||||
'login_history_list' => UserLoginAttempt::all($loginHistoryPagination, $currentUser),
|
||||
'login_history_pagination' => $loginHistoryPagination,
|
||||
'account_log_list' => AuditLog::all($accountLogPagination, $currentUser),
|
||||
'account_log_pagination' => $accountLogPagination,
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Misuzu\Users;
|
|||
use Misuzu\Colour;
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Memoizer;
|
||||
use Misuzu\TOTP;
|
||||
use Misuzu\Net\IPAddress;
|
||||
|
||||
class UserException extends UsersException {} // this naming definitely won't lead to confusion down the line!
|
||||
|
@ -36,6 +37,8 @@ class User {
|
|||
|
||||
private static $localUser = null;
|
||||
|
||||
private $totp = null;
|
||||
|
||||
private const USER_SELECT = '
|
||||
SELECT u.`user_id`, u.`username`, u.`password`, u.`email`, u.`user_super`, u.`user_title`,
|
||||
u.`user_country`, u.`user_colour`, u.`display_role`, u.`user_totp_key`,
|
||||
|
@ -110,6 +113,21 @@ class User {
|
|||
public function hasTOTP(): bool {
|
||||
return !empty($this->user_totp_key);
|
||||
}
|
||||
public function getTOTP(): TOTP {
|
||||
if($this->totp === null)
|
||||
$this->totp = new TOTP($this->user_totp_key);
|
||||
return $this->totp;
|
||||
}
|
||||
public function getValidTOTPTokens(): array {
|
||||
if(!$this->hasTOTP())
|
||||
return [];
|
||||
$totp = $this->getTOTP();
|
||||
return [
|
||||
$totp->generate(time()),
|
||||
$totp->generate(time() - 30),
|
||||
$totp->generate(time() + 30),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBackgroundSettings(): int { // Use the below methods instead
|
||||
return $this->user_background_settings;
|
||||
|
|
123
src/Users/UserLoginAttempt.php
Normal file
123
src/Users/UserLoginAttempt.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
namespace Misuzu\Users;
|
||||
|
||||
use Misuzu\DB;
|
||||
use Misuzu\Pagination;
|
||||
use Misuzu\Net\IPAddress;
|
||||
|
||||
class UserLoginAttempt {
|
||||
// Database fields
|
||||
private $user_id = null;
|
||||
private $attempt_success = false;
|
||||
private $attempt_ip = '::1';
|
||||
private $attempt_country = 'XX';
|
||||
private $attempt_created = null;
|
||||
private $attempt_user_agent = '';
|
||||
|
||||
private $user = null;
|
||||
private $userLookedUp = false;
|
||||
|
||||
public const TABLE = 'login_attempts';
|
||||
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||||
private const SELECT = '%1$s.`user_id`, %1$s.`attempt_success`, %1$s.`attempt_country`, %1$s.`attempt_user_agent`'
|
||||
. ', INET6_NTOA(%1$s.`attempt_ip`) AS `attempt_ip`'
|
||||
. ', UNIX_TIMESTAMP(%1$s.`attempt_created`) AS `attempt_created`';
|
||||
|
||||
public function getUserId(): int {
|
||||
return $this->user_id < 1 ? -1 : $this->user_id;
|
||||
}
|
||||
public function getUser(): ?User {
|
||||
if(!$this->userLookedUp && ($userId = $this->getUserId()) > 0) {
|
||||
$this->userLookedUp = true;
|
||||
try {
|
||||
$this->user = User::byId($userId);
|
||||
} catch(UserNotFoundException $ex) {}
|
||||
}
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function isSuccess(): bool {
|
||||
return boolval($this->attempt_success);
|
||||
}
|
||||
|
||||
public function getRemoteAddress(): string {
|
||||
return $this->attempt_ip;
|
||||
}
|
||||
|
||||
public function getCountry(): string {
|
||||
return $this->attempt_country;
|
||||
}
|
||||
public function getCountryName(): string {
|
||||
return get_country_name($this->getCountry());
|
||||
}
|
||||
|
||||
public function getCreatedTime(): int {
|
||||
return $this->attempt_created === null ? -1 : $this->attempt_created;
|
||||
}
|
||||
|
||||
public function getUserAgent(): string {
|
||||
return $this->attempt_user_agent;
|
||||
}
|
||||
|
||||
public static function remaining(?string $remoteAddr = null): int {
|
||||
$remoteAddr = $ipAddress ?? IPAddress::remote();
|
||||
return (int)DB::prepare(
|
||||
'SELECT 5 - COUNT(*)'
|
||||
. ' FROM `' . DB::PREFIX . self::TABLE . '`'
|
||||
. ' WHERE `attempt_success` = 0'
|
||||
. ' AND `attempt_created` > NOW() - INTERVAL 1 HOUR'
|
||||
. ' AND `attempt_ip` = INET6_ATON(:remote_ip)'
|
||||
) ->bind('remote_ip', $remoteAddr)
|
||||
->fetchColumn();
|
||||
}
|
||||
|
||||
public static function create(bool $success, ?User $user = null, ?string $remoteAddr = null, string $userAgent = null): void {
|
||||
$remoteAddr = $ipAddress ?? IPAddress::remote();
|
||||
$userAgent = $userAgent ?? filter_input(INPUT_SERVER, 'HTTP_USER_AGENT', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) ?? '';
|
||||
$createLog = DB::prepare(
|
||||
'INSERT INTO `' . DB::PREFIX . self::TABLE . '` (`user_id`, `attempt_success`, `attempt_ip`, `attempt_country`, `attempt_user_agent`)'
|
||||
. ' VALUES (:user, :success, INET6_ATON(:ip), :country, :user_agent)'
|
||||
) ->bind('user', $user === null ? null : $user->getId()) // this null situation should never ever happen but better safe than sorry !
|
||||
->bind('success', $success ? 1 : 0)
|
||||
->bind('ip', $remoteAddr)
|
||||
->bind('country', IPAddress::country($remoteAddr))
|
||||
->bind('user_agent', $userAgent)
|
||||
->execute();
|
||||
}
|
||||
|
||||
private static function countQueryBase(): string {
|
||||
return sprintf(self::QUERY_SELECT, sprintf('COUNT(*)', self::TABLE));
|
||||
}
|
||||
public static function countAll(?User $user = null): int {
|
||||
$getCount = DB::prepare(
|
||||
self::countQueryBase()
|
||||
. ($user === null ? '' : ' WHERE `user_id` = :user')
|
||||
);
|
||||
if($user !== null)
|
||||
$getCount->bind('user', $user->getId());
|
||||
return (int)$getCount->fetchColumn();
|
||||
}
|
||||
|
||||
private static function byQueryBase(): string {
|
||||
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
|
||||
}
|
||||
public static function all(?Pagination $pagination = null, ?User $user = null): array {
|
||||
$attemptsQuery = self::byQueryBase()
|
||||
. ($user === null ? '' : ' WHERE `user_id` = :user')
|
||||
. ' ORDER BY `attempt_created` DESC';
|
||||
|
||||
if($pagination !== null)
|
||||
$attemptsQuery .= ' LIMIT :range OFFSET :offset';
|
||||
|
||||
$getAttempts = DB::prepare($attemptsQuery);
|
||||
|
||||
if($user !== null)
|
||||
$getAttempts->bind('user', $user->getId());
|
||||
|
||||
if($pagination !== null)
|
||||
$getAttempts->bind('range', $pagination->getRange())
|
||||
->bind('offset', $pagination->getOffset());
|
||||
|
||||
return $getAttempts->fetchObjects(self::class);
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
<?php
|
||||
function user_login_attempt_record(bool $success, ?int $userId, string $ipAddress, string $userAgent): void {
|
||||
$storeAttempt = \Misuzu\DB::prepare('
|
||||
INSERT INTO `msz_login_attempts`
|
||||
(`attempt_success`, `attempt_ip`, `attempt_country`, `user_id`, `attempt_user_agent`)
|
||||
VALUES
|
||||
(:attempt_success, INET6_ATON(:attempt_ip), :attempt_country, :user_id, :attempt_user_agent)
|
||||
');
|
||||
|
||||
$storeAttempt->bind('attempt_success', $success ? 1 : 0);
|
||||
$storeAttempt->bind('attempt_ip', $ipAddress);
|
||||
$storeAttempt->bind('attempt_country', \Misuzu\Net\IPAddress::country($ipAddress));
|
||||
$storeAttempt->bind('attempt_user_agent', $userAgent);
|
||||
$storeAttempt->bind('user_id', $userId, $userId === null ? PDO::PARAM_NULL : PDO::PARAM_INT);
|
||||
$storeAttempt->execute();
|
||||
}
|
||||
|
||||
function user_login_attempts_remaining(string $ipAddress): int {
|
||||
$getRemaining = \Misuzu\DB::prepare('
|
||||
SELECT 5 - COUNT(`attempt_id`)
|
||||
FROM `msz_login_attempts`
|
||||
WHERE `attempt_success` = 0
|
||||
AND `attempt_created` > NOW() - INTERVAL 1 HOUR
|
||||
AND `attempt_ip` = INET6_ATON(:remote_ip)
|
||||
');
|
||||
$getRemaining->bind('remote_ip', $ipAddress);
|
||||
|
||||
return (int)$getRemaining->fetchColumn();
|
||||
}
|
||||
|
||||
function user_login_attempts_count($userId = 0): int {
|
||||
$getCount = \Misuzu\DB::prepare(sprintf('
|
||||
SELECT COUNT(`attempt_id`)
|
||||
FROM `msz_login_attempts`
|
||||
WHERE %s
|
||||
', $userId < 1 ? '1' : '`user_id` = :user_id'));
|
||||
|
||||
if($userId >= 1) {
|
||||
$getCount->bind('user_id', $userId);
|
||||
}
|
||||
|
||||
return (int)$getCount->fetchColumn();
|
||||
}
|
||||
|
||||
function user_login_attempts_list(int $offset, int $take, int $userId = 0): array {
|
||||
$offset = max(0, $offset);
|
||||
$take = max(1, $take);
|
||||
|
||||
$getAttempts = \Misuzu\DB::prepare(sprintf('
|
||||
SELECT
|
||||
`attempt_id`, `attempt_country`, `attempt_success`, `attempt_user_agent`, `attempt_created`,
|
||||
INET6_NTOA(`attempt_ip`) as `attempt_ip`
|
||||
FROM `msz_login_attempts`
|
||||
WHERE %s
|
||||
ORDER BY `attempt_id` DESC
|
||||
LIMIT :offset, :take
|
||||
', $userId < 1 ? '1' : '`user_id` = :user_id'));
|
||||
|
||||
if($userId > 0) {
|
||||
$getAttempts->bind('user_id', $userId);
|
||||
}
|
||||
|
||||
$getAttempts->bind('offset', $offset);
|
||||
$getAttempts->bind('take', $take);
|
||||
|
||||
return $getAttempts->fetchAll();
|
||||
}
|
|
@ -211,12 +211,12 @@
|
|||
{% endmacro %}
|
||||
|
||||
{% macro user_login_attempt(attempt) %}
|
||||
{% set browser = get_browser(attempt.attempt_user_agent) %}
|
||||
{% set browser = get_browser(attempt.userAgent) %}
|
||||
|
||||
<div class="settings__login-attempt{% if not attempt.attempt_success %} settings__login-attempt--failed{% endif %}" id="login-attempt-{{ attempt.attempt_id }}">
|
||||
<div class="settings__login-attempt{% if not attempt.success %} settings__login-attempt--failed{% endif %}">
|
||||
<div class="settings__login-attempt__container">
|
||||
<div class="settings__login-attempt__important">
|
||||
<div class="flag flag--{{ attempt.attempt_country|lower }} settings__login-attempt__flag" title="{{ attempt.attempt_country|country_name }}">{{ attempt.attempt_country }}</div>
|
||||
<div class="flag flag--{{ attempt.country|lower }} settings__login-attempt__flag" title="{{ attempt.countryName }}">{{ attempt.country }}</div>
|
||||
|
||||
<div class="settings__login-attempt__description">
|
||||
{{ browser.browser }} on {{ browser.platform }}
|
||||
|
@ -229,7 +229,7 @@
|
|||
IP Address
|
||||
</div>
|
||||
<div class="settings__login-attempt__detail__value">
|
||||
{{ attempt.attempt_ip }}
|
||||
{{ attempt.remoteAddress }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -238,16 +238,16 @@
|
|||
Succeeded
|
||||
</div>
|
||||
<div class="settings__login-attempt__detail__value">
|
||||
{{ attempt.attempt_success ? 'Yes' : 'No' }}
|
||||
{{ attempt.success ? 'Yes' : 'No' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings__login-attempt__detail" title="{{ attempt.attempt_created|date('r') }}">
|
||||
<div class="settings__login-attempt__detail" title="{{ attempt.createdTime|date('r') }}">
|
||||
<div class="settings__login-attempt__detail__title">
|
||||
Attempted
|
||||
</div>
|
||||
<time class="settings__login-attempt__detail__value" datetime="{{ attempt.attempt_created|date('c') }}">
|
||||
{{ attempt.attempt_created|time_diff }}
|
||||
<time class="settings__login-attempt__detail__value" datetime="{{ attempt.createdTime|date('c') }}">
|
||||
{{ attempt.createdTime|time_diff }}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
|
@ -256,7 +256,7 @@
|
|||
User Agent
|
||||
</div>
|
||||
<div class="settings__login-attempt__detail__value">
|
||||
{{ attempt.attempt_user_agent|length > 0 ? attempt.attempt_user_agent : 'None' }}
|
||||
{{ attempt.userAgent is empty ? 'None' : attempt.userAgent }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue