Skip to content

Commit

Permalink
feat: Add a page to view last authentications attempts
Browse files Browse the repository at this point in the history
  • Loading branch information
geokrety-bot authored and kumy committed Jul 31, 2023
1 parent 286d875 commit dbf3b43
Show file tree
Hide file tree
Showing 15 changed files with 274 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{if $authentications.subset}
{foreach from=$authentications.subset item=authentication}
<tr class="{if !$authentication->succeed}danger{else}success{/if}">
<td>
{if $authentication->succeed}{t}Success{/t}{else}{t}Failure{/t}{/if}
</td>
<td>
{$authentication->method}
</td>
<td>
{$authentication->created_on_datetime|print_date_iso_format nofilter}
</td>
<td>
{$authentication->ip}
</td>
<td>
{$authentication->user_agent}
</td>
</tr>
{/foreach}
{/if}
11 changes: 7 additions & 4 deletions website/app-templates/smarty/macros/datatable.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
ajax: {
"url": '{$alias|alias}',
"dataSrc": function(json) {
var data = []
var i = 0
// Convert html table rows to json object array
// (loosing td attributes and styles)
var data = [];
var i = 0;
$(json.data).find('tr').each(function() {
data.push([]);
data.push({ "DT_RowClass": $(this).attr('class') });
var j = 0;
$(this).find('td').each(function() {
data[i].push($(this).html());
data[i][j++] = $(this).html();
})
i++;
})
Expand Down
59 changes: 59 additions & 0 deletions website/app-templates/smarty/pages/user_authentication_history.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{extends file='base.tpl'}

{block name=title}🙋 {t}Authentication history{/t}{/block}

{\Assets::instance()->addCss(GK_CDN_DATATABLE_CSS) && ''}
{\Assets::instance()->addJs(GK_CDN_DATATABLE_JS) && ''}

{include file='macros/pagination.tpl'}
{block name=content}
<a class="anchor" id="results"></a>

<h2>🙋 {t}Authentication history{/t}</h2>
<div class="row">
<div class="col-xs-12 col-md-9">

{if $authentications_count}
<div class="table-responsive">
<table id="userAuthenticationHistory" class="table table-striped" style="width:100%">
<thead>
<tr>
<th>{t}Status{/t}</th>
<th>{t}Method{/t}</th>
<th>{t}Date{/t}</th>
<th>{t}IP Address{/t}</th>
<th>{t}User-Agent{/t}</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
{else}

<em>{t}No activity yet!{/t}</em>

{/if}

</div>
<div class="col-xs-12 col-md-3">
{* {include file='blocks/user/actions.tpl'}*}
</div>
</div>

{/block}

{include file='macros/datatable.tpl'}
{block name=javascript}
$('#userAuthenticationHistory').dataTable({
{call common alias='user_authentication_history'}
"searching": false,
"order": [[ 2, 'desc' ]],
"columns": [
{ "name": "succeed" },
{ "name": "method" },
{ "name": "created_on_datetime" },
{ "name": "ip" },
{ "name": "user_agent" }
],
});
{/block}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

use Carbon\Carbon;

/**
* Smarty plugin
* -------------------------------------------------------------
* File: modifier.print_date_iso_format.php
* Type: modifier
* Name: print_date_iso_format
* Purpose: outputs a date time as isoformat
* -------------------------------------------------------------.
*/
function smarty_modifier_print_date_iso_format(DateTime $date, string $isoFormat = 'lll', string $format = 'c'): string {
return sprintf(
'<span data-datetime="%s" title="%s">%s</span>',
$date->format($format),
$date->format($format),
Carbon::parse($date->format('c'))->isoFormat($isoFormat)
);
}
75 changes: 69 additions & 6 deletions website/app/GeoKrety/Controller/Pages/BaseDatatable.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@
use GeoKrety\Model\Geokret;
use GeoKrety\Service\Smarty;

/**
* Render a database query in datatable compatible JSON format.
*/
abstract class BaseDatatable extends Base {
/**
* Display results as json.
*
* @return void
*
* @throws \SmartyException
*/
public function asDataTable(\Base $f3) {
$response = [
'draw' => (int) $f3->get('GET.draw'),
Expand All @@ -28,6 +38,7 @@ public function asDataTable(\Base $f3) {
try {
$subset = $object->paginate($start, $f3->get('GET.length'), $filter, $option);
} catch (\PDOException $e) {
\Sentry\captureException($e);
$response['error'] = _('This query is invalid');
echo json_encode($response);
exit();
Expand Down Expand Up @@ -77,6 +88,11 @@ protected function datatable_get_parameters(\Base $f3, \DB\Cortex $obj) {
return false;
}

/**
* Build the sql "ORDER BY" query part.
*
* @return string The sql "ORDER BY" query part
*/
protected function datatable_build_order(\Base $f3): string {
$orders = $f3->get('GET.order');
$columns = $f3->get('GET.columns');
Expand All @@ -88,25 +104,45 @@ protected function datatable_build_order(\Base $f3): string {
return join(', ', $order);
}

/**
* Build the sql "WHERE" query part.
*
* @param array $searchable The allowed columns to be searchable
* @param array $filter A custom static filter to also apply
*
* @return array The sql "WHERE" query part, and an array with the values
*/
protected function datatable_build_search(\Base $f3, array $searchable, array $filter = []): array {
$search = $f3->get('GET.search.value');
if (empty($search)) {
return $filter;
}
$searches = [];
$searches_values = array_fill(0, sizeof($searchable), "%$search%");
$searches_values = [];
// Special case if the first column is "gkid"
if ($searchable[0] === 'gkid') {
$s = array_shift($searchable);
$gkid = Geokret::gkid2id($search);
if (!is_null($gkid)) {
$searches[] = "$s = ?";
$searches_values[0] = $gkid;
} else {
array_shift($searches_values);
$searches_values[] = $gkid;
}
}
foreach ($searchable as $s) {
if (is_array($s)) {
$searches[] = "$s[0] $s[1] ?";
$searches_values[] = "$search";
continue;
} elseif (str_ends_with($s, '_datetime')) {
// Not easy to implement properly
// so never allow search by datetime
continue;
} elseif ($s === 'ip') {
$searches[] = "TEXT($s) like ?";
$searches_values[] = "%$search%";
continue;
}
$searches_values[] = "%$search%";
$searches[] = "$s like ?";
}
$filters = sizeof($filter) ? [array_shift($filter)] : [];
Expand All @@ -116,23 +152,50 @@ protected function datatable_build_search(\Base $f3, array $searchable, array $f
return [$query, ...(array_merge($filter, $searches_values))];
}

/**
* An optional static filter to apply.
*
* @return array The sql "WHERE" query part, and an array with the values
*/
protected function getFilter(): array {
return [];
}

/**
* An optional static "HAS" filter to apply.
*
* @return array The sql "WHERE" query part, and an array with the values
*/
protected function getHas(\GeoKrety\Model\Base $object): void {
}

/**
* @return string[]
* An array of searchable columns.
*
* @return string[] Searchable columns
*/
protected function getSearchable(): array {
return ['gkid', 'name'];
return [];
}

/**
* Must return a new instance of the Model object to query.
*
* @return \GeoKrety\Model\Base The model object
*/
abstract protected function getObject(): \GeoKrety\Model\Base;

/**
* The name of the variable that will contain the results in Smarty.
*
* @return string smarty variable name
*/
abstract protected function getObjectName(): string;

/**
* The smarty template name to apply on each row result.
*
* @return string smarty template name
*/
abstract protected function getTemplate(): string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@ protected function getObject(): \GeoKrety\Model\Base {
protected function getObjectName(): string {
return 'geokrety';
}

/**
* @return string[]
*/
protected function getSearchable(): array {
return ['gkid', 'name'];
}
}
7 changes: 7 additions & 0 deletions website/app/GeoKrety/Controller/Pages/BaseDatatableMoves.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@ protected function getObject(): \GeoKrety\Model\Base {
protected function getObjectName(): string {
return 'move';
}

/**
* @return string[]
*/
protected function getSearchable(): array {
return ['gkid', 'name'];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace GeoKrety\Controller;

use GeoKrety\Model\UsersAuthenticationHistory;

abstract class BaseDatatableUserAuthenticationHistory extends BaseDatatable {
protected function getObject(): \GeoKrety\Model\Base {
return new UsersAuthenticationHistory();
}

protected function getObjectName(): string {
return 'authentications';
}

/**
* @return string[]
*/
protected function getSearchable(): array {
return [];
}
}
12 changes: 12 additions & 0 deletions website/app/GeoKrety/Controller/Pages/Login.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use GeoKrety\AuthGroup;
use GeoKrety\Model\SocialAuthProvider;
use GeoKrety\Model\User;
use GeoKrety\Model\UsersAuthenticationHistory;
use GeoKrety\Model\UserSocialAuth;
use GeoKrety\Service\LanguageService;
use GeoKrety\Service\RateLimit;
Expand Down Expand Up @@ -98,6 +99,17 @@ public static function connectUser(\Base $f3, User $user, ?string $method = null
Session::setGKTCookie();
LanguageService::changeLanguageTo($user->preferred_language);
Flash::instance()->addMessage(_('Welcome on board!'), 'success');
$failed_count = UsersAuthenticationHistory::has_failed_attempts($user->username);
if ($failed_count) {
Flash::instance()->addMessage(
sprintf(
_('There was %d failed login attempts on your account, please review <a href="%s">login activity</a>'),
$failed_count,
$f3->alias('user_authentication_history'),
),
'warning'
);
}
if ($redirect) {
$url = Url::unserializeGoto($user->preferred_language);
if (is_null($url)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace GeoKrety\Controller;

use CurrentUserLoader;
use GeoKrety\Model\UsersAuthenticationHistory;
use GeoKrety\Service\Smarty;

class UserAuthenticationHistory extends BaseDatatableUserAuthenticationHistory {
use CurrentUserLoader;

public function get(\Base $f3) {
$authentications_count = new UsersAuthenticationHistory();
Smarty::assign('authentications_count', $authentications_count->count($this->getFilter()));
Smarty::render('pages/user_authentication_history.tpl');
}

protected function getFilter(): array {
return ['username = ?', $this->currentUser->username];
}

protected function getTemplate(): string {
return 'elements/user_authentication_history_as_list.tpl';
}
}
12 changes: 12 additions & 0 deletions website/app/GeoKrety/Model/UsersAuthenticationHistory.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ public static function save_authentication_history(string $username, $method, ?U
$history->save();
}

public static function has_failed_attempts(string $username): int {
$sql = <<<'EOT'
SELECT COUNT(*) as failed_count
FROM previous_failed_logins(?)
EOT;
$result = \Base::instance()->get('DB')->exec($sql, [
$username,
]);

return $result[0]['failed_count'];
}

public function jsonSerialize() {
return [
'user' => $this->getRaw('user'),
Expand Down
1 change: 1 addition & 0 deletions website/app/authorizations.ini
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ allow @user_observation_area = authenticated,admin,superadmin
allow @user_template_chooser = authenticated,admin,superadmin
allow @user_delete_account = authenticated,admin,superadmin
allow POST @user_setting_update = authenticated,admin,superadmin
allow @user_authentication_history = authenticated,admin,superadmin
allow @geokret_create = authenticated,admin,superadmin
allow @geokret_edit = authenticated,admin,superadmin
allow @geokrety_move_edit = authenticated,admin,superadmin
Expand Down
Loading

0 comments on commit dbf3b43

Please sign in to comment.