admin: auth and action logs

This commit is contained in:
E. S. 2024-03-14 12:03:48 +00:00
parent 72ed74e789
commit caeafc3f54
7 changed files with 260 additions and 16 deletions

View File

@ -1,6 +1,6 @@
<?php
const ROUTER_VERSION = 5;
const ROUTER_VERSION = 6;
const ROUTER_MC_KEY = '4in1/routes';
$RouterInput = [];

View File

@ -47,7 +47,7 @@ class AdminHandler extends request_handler {
= input('i:ip, query, url_query, file_query, i:line_query, i:per_page');
if (!$per_page)
$per_page = 10;
$per_page = 100;
$db = DB();
$query = trim($query ?? '');
@ -148,6 +148,94 @@ class AdminHandler extends request_handler {
...$vars);
}
function GET_auth_log() {
$db = DB();
$count = (int)$db->result($db->query("SELECT COUNT(*) FROM admin_log"));
$per_page = 100;
list($page, $pages, $offset) = get_page($per_page, $count);
$q = $db->query("SELECT *,
INET_NTOA(ip) AS ip,
admins.login AS login,
admins.activity_ts AS activitiy_ts
FROM admin_log
LEFT JOIN admins ON admins.id=admin_log.admin_id
ORDER BY admin_log.id DESC
LIMIT $offset, ".$per_page);
$list = $db->fetchAll($q);
if (!empty($list)) {
$list = array_map(function($item) {
$item['date'] = format_time($item['ts']);
$item['activity_ts_s'] = format_time($item['activity_ts']);
return $item;
}, $list);
}
$vars = [
'list' => $list,
'pn_page' => $page,
'pn_pages' => $pages
];
set_skin_opts(['wide' => true]);
set_title('$admin_auth_log');
render('admin/auth_log',
...$vars);
}
function GET_actions_log() {
$field_types = \AdminActions\Util\Logger::getFieldTypes();
foreach ($field_types as $type_prefix => $type_data) {
for ($i = 1; $i <= $type_data['count']; $i++) {
$name = $type_prefix.'arg'.$i;
if (isset($_REQUEST[$name]) && (string)$_REQUEST[$name] !== '')
$argument_filters[$name] = $type_data['unpacker']((string)$_REQUEST[$name]);
}
}
$per_page = 100;
$count = \AdminActions\Util\Logger::getRecordsCount();
list($page, $pages, $offset) = get_page($per_page, $count);
$admin_ids = [];
$admin_logins = [];
$records = \AdminActions\Util\Logger::getRecords($offset, $per_page);
foreach ($records as $record) {
list($admin_id) = $record->getActorInfo();
if ($admin_id !== null)
$admin_ids[$admin_id] = true;
}
if (!empty($admin_ids))
$admin_logins = admin_get_logins_by_id(array_keys($admin_ids));
$url = '/admin/actions-log/?';
$filter_fields = [];
foreach ($field_types as $type_prefix => $type_data) {
for ($i = 1; $i <= $type_data['count']; $i++) {
$name = $type_prefix.'arg'.$i;
$filter_fields[$name] = $argument_filters[$name] ?? '';
}
}
$vars = [
'list' => $records,
'pn_page' => $page,
'pn_pages' => $pages,
'admin_logins' => $admin_logins,
'url' => $url,
'action_types' => \AdminActions\Util\Logger::getActions(true),
];
set_skin_opts(['wide' => true]);
set_title('$admin_actions_log');
render('admin/actions_log',
...$vars);
}
function GET_uploads() {
list($error) = input('error');
$uploads = uploads::getAllUploads();

View File

@ -54,9 +54,9 @@ abstract class BaseAction {
return $this->isCli;
}
//public function getDate(): string {
// return formatTime($this->timeStamp, ['short_months' => true]);
//}
public function getDate(): string {
return format_time($this->timeStamp, ['short_months' => true]);
}
public function getTimeStamp(): int {
return $this->timeStamp;
@ -70,7 +70,7 @@ abstract class BaseAction {
return $this->recordId;
}
/*function renderHtml(): string {
function renderHtml(): string {
$rc = new \ReflectionClass($this);
$lines = [];
$fields = $rc->getProperties(\ReflectionProperty::IS_PUBLIC);
@ -87,5 +87,5 @@ abstract class BaseAction {
$lines[] = $name.'='.$val;
}
return implode('<br>', $lines);
}*/
}
}

View File

@ -54,6 +54,20 @@ function admin_delete(string $login): bool {
return true;
}
/**
* @param int[] $ids
* @return string[]
*/
function admin_get_logins_by_id(array $ids): array {
$db = DB();
$logins = [];
$q = $db->query("SELECT id, login FROM admins WHERE id IN (".implode(',', $ids).")");
while ($row = $db->fetch($q)) {
$logins[(int)$row['id']] = $row['login'];
}
return $logins;
}
function admin_get_id_by_login(string $login): ?int {
$db = DB();
$q = $db->query("SELECT id FROM admins WHERE login=?", $login);

View File

@ -25,15 +25,16 @@ return (function() {
'robots.txt' => 'robots_txt',
],
'Admin' => [
'admin/' => 'index',
'admin/{login,logout,log}/' => '${1}',
'([a-z0-9-]+)/{delete,edit}/' => 'page_${1} short_name=$(1)',
'([a-z0-9-]+)/create/' => 'page_add short_name=$(1)',
'articles/write/' => 'post_add',
'articles/([a-z0-9-]+)/{delete,edit}/' => 'post_${1} short_name=$(1)',
'admin/markdown-preview.ajax' => 'ajax_md_preview',
'admin/{uploads,errors}/' => '${1}',
'admin/uploads/{edit_note,delete}/(\d+)/' => 'upload_${1} id=$(1)'
'admin/' => 'index',
'admin/{login,logout,log}/' => '${1}',
'([a-z0-9-]+)/{delete,edit}/' => 'page_${1} short_name=$(1)',
'([a-z0-9-]+)/create/' => 'page_add short_name=$(1)',
'articles/write/' => 'post_add',
'articles/([a-z0-9-]+)/{delete,edit}/' => 'post_${1} short_name=$(1)',
'admin/markdown-preview.ajax' => 'ajax_md_preview',
'admin/{uploads,errors}/' => '${1}',
'admin/{auth,actions}-log/' => '${1}_log',
'admin/uploads/{edit_note,delete}/(\d+)/' => 'upload_${1} id=$(1)'
]
];

View File

@ -3,6 +3,7 @@
namespace skin\admin;
use Stringable;
use function skin\base\layout;
// login page
// ----------
@ -53,6 +54,8 @@ function index($ctx, $admin_login) {
<!--<a href="/admin/log/">Log</a><br/>-->
<a href="/admin/uploads/">Uploads</a><br>
<a href="/admin/errors/">{$ctx->lang('admin_errors')}</a><br>
<a href="/admin/auth-log/">{$ctx->lang('admin_auth_log')}</a><br>
<a href="/admin/actions-log/">{$ctx->lang('admin_actions_log')}</a><br>
</div>
HTML;
}
@ -422,6 +425,12 @@ return <<<HTML
HTML;
}
// ----------------------------------------------------
// ---------------------- ERRORS ----------------------
// ----------------------------------------------------
function errors($ctx,
array $list,
int $count,
@ -544,4 +553,134 @@ return <<<HTML
<div id="admin_error_log_stacktrace{$item_id}" style="display: none">{$nl2br_stacktrace}</div>
</div>
HTML;
}
// ------------------------------------------------------
// ---------------------- AUTH LOG ----------------------
// ------------------------------------------------------
function auth_log($ctx, array $list, int $pn_page, int $pn_pages) {
return <<<HTML
{$ctx->bc([
['text' => $ctx->lang('admin_title'), 'url' => '/admin/'],
['text' => $ctx->lang('admin_auth_log')],
])}
{$ctx->if_then_else(!empty($list),
fn() => $ctx->auth_log_table($list),
fn() => '<div class="empty_block">Auth log is empty.</div>')}
{$ctx->pagenav($pn_page, $pn_pages, '/admin/auth-log/?page={page}')}
HTML;
}
function auth_log_table($ctx, array $list) {
return <<<HTML
<table border="1" width="100%" cellpadding="0" cellspacing="0" class="admin-error-log">
<thead>
<tr>
<th width="20%">Admin</th>
<th width="15%">Time</th>
<th width="10%">IP</th>
<th width="55%">User-Agent</th>
</tr>
</thead>
<tbody>
{$ctx->for_each($list,
fn($item) => $ctx->auth_log_table_item(
date: $item['date'],
ip: $item['ip'],
user_agent: $item['ua'],
admin_login: $item['login'],
admin_id: (int)$item['admin_id'],
activity_ts: $item['activity_ts_s']))}
</tbody>
</table>
HTML;
}
function auth_log_table_item($ctx, $date, $ip, $user_agent, string $admin_login, int $admin_id, string $activity_ts) {
return <<<HTML
<tr>
<td>
<span title="Last activity: {$activity_ts}">{$admin_login}</span> (id={$admin_id})
</td>
<td>{$date}</td>
<td>{$ip}</td>
<td>{$user_agent}</td>
</tr>
HTML;
}
// ---------------------------------------------------------
// ---------------------- ACTIONS LOG ----------------------
// ---------------------------------------------------------
function actions_log($ctx,
array $list,
array $admin_logins,
string $url,
array $action_types,
int $pn_page,
int $pn_pages) {
return <<<HTML
{$ctx->bc([
['text' => $ctx->lang('admin_title'), 'url' => '/admin/'],
['text' => $ctx->lang('admin_actions_log')],
])}
{$ctx->if_then_else(!empty($list),
fn() => $ctx->actions_log_table($list, $admin_logins, $action_types),
fn() => '<div class="empty_block">Actions log is empty.</div>')}
{$ctx->pagenav($pn_page, $pn_pages, $url.'page={page}')}
HTML;
}
function actions_log_table($ctx, array $list, array $admin_logins, array $action_types) {
return <<<HTML
<table border="1" width="100%" cellpadding="0" cellspacing="0" class="admin-error-log">
<thead>
<tr>
<th width="9%">Time</th>
<th width="14%">Who</th>
<th width="11%">Action</th>
<th>Data</th>
</tr>
</thead>
<tbody>
{$ctx->for_each($list,
fn(\AdminActions\BaseAction $item) => $ctx->actions_log_table_item(
date: $item->getDate(),
ip: $item->getIPv4(),
is_cli: $item->isCommandLineAction(),
admin_login: $admin_logins[$item->getAdminId()],
action_name: $item->getActionName(),
unsafe_data: $item->renderHtml()))}
</tbody>
</table>
HTML;
}
function actions_log_table_item($ctx,
string $date,
string $ip,
bool $is_cli,
string $admin_login,
string $action_name,
string $unsafe_data) {
return <<<HTML
<tr>
<td>{$date}</td>
<td>
{$ctx->if_then_else(!$is_cli,
fn() => $admin_login.', '.$ip,
fn() => 'console')}
</td>
<td>{$action_name}</td>
<td>{$unsafe_data}</td>
</tr>
HTML;
}

View File

@ -97,6 +97,8 @@ admin_password: Password
admin_login: Login
admin_books: Books
admin_errors: Errors
admin_auth_log: Auth log
admin_actions_log: Actions log
# /files
files: Files