admin: auth and action logs
This commit is contained in:
parent
72ed74e789
commit
caeafc3f54
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
const ROUTER_VERSION = 5;
|
||||
const ROUTER_VERSION = 6;
|
||||
const ROUTER_MC_KEY = '4in1/routes';
|
||||
|
||||
$RouterInput = [];
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}*/
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
19
routes.php
19
routes.php
@ -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)'
|
||||
]
|
||||
];
|
||||
|
||||
|
139
skin/admin.phps
139
skin/admin.phps
@ -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;
|
||||
}
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user