Update audit logging and dashboard
This commit is contained in:
@ -10,14 +10,29 @@
|
||||
<td
|
||||
class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400"
|
||||
>
|
||||
{{ entry.subsystem }}
|
||||
</td>
|
||||
<td
|
||||
class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400"
|
||||
>
|
||||
<pre><code class="language-json">
|
||||
{%- set entry_object = ({"object": entry.object, "object_id": entry.object_id, "client_id": entry.client_id, "client_name": entry.client_name}) -%}
|
||||
{{- entry_object | tojson(indent=2) -}}</code></pre>
|
||||
<span class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-gray-700 dark:text-gray-300">{{ entry.subsystem }}</span>
|
||||
|
||||
<span class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-gray-700 dark:text-gray-300">{{ entry.operation }}</span>
|
||||
|
||||
{% if entry.client_id %}
|
||||
<span class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-gray-700 dark:text-gray-300">
|
||||
Client: <abbr title="{{ entry.client_id }}">{{ entry.client_name }}</abbr>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if entry.secret_name %}
|
||||
<span class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-gray-700 dark:text-gray-300">
|
||||
Secret:<abbr title="{{ entry.secret_id }}">{{ entry.secret_name }}</abbr>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if entry.data %}
|
||||
{% for key, value in entry.data.items() %}
|
||||
|
||||
<span class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-gray-700 dark:text-gray-300">
|
||||
{{ key }}:{{ value }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
<td
|
||||
class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400"
|
||||
@ -29,3 +44,5 @@
|
||||
>
|
||||
{{ entry.origin }}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
@ -3,48 +3,118 @@
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-block min-w-full align-middle">
|
||||
<div class="overflow-hidden shadow">
|
||||
<table
|
||||
class="min-w-full divide-y divide-gray-200 table-fixed dark:divide-gray-600"
|
||||
>
|
||||
<thead class="bg-gray-100 dark:bg-gray-700">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-4 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Timestamp
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-4 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
Subsystem
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
<a id="filterSubsystem" data-dropdown-toggle="filterSubsystemsDropdown" class="whitespace-nowrap inline-flex items-center font-medium text-gray-500 hover:underline">
|
||||
Subsystem <svg class="w-[12px] h-[12px] text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M18.425 10.271C19.499 8.967 18.57 7 16.88 7H7.12c-1.69 0-2.618 1.967-1.544 3.271l4.881 5.927a2 2 0 0 0 3.088 0l4.88-5.927Z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
|
||||
</a>
|
||||
<div id="filterSubsystemsDropdown" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600">
|
||||
<div class="py-2">
|
||||
<a href="?" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">All</a>
|
||||
</div>
|
||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="filterSubsystem">
|
||||
<li>
|
||||
<a href="?subsystem=admin" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Admin</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?subsystem=sshd" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Ssh Server</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="?subsystem=backend" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Backend</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-4 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
Object
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
<a id="filterOperation" data-dropdown-toggle="filterOperationsDropdown" class="whitespace-nowrap inline-flex items-center font-medium text-gray-500 hover:underline">
|
||||
Operation <svg class="w-[12px] h-[12px] text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M18.425 10.271C19.499 8.967 18.57 7 16.88 7H7.12c-1.69 0-2.618 1.967-1.544 3.271l4.881 5.927a2 2 0 0 0 3.088 0l4.88-5.927Z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</a>
|
||||
<div id="filterOperationsDropdown" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600">
|
||||
<div class="py-2">
|
||||
<a href="?" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">All</a>
|
||||
</div>
|
||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="filterSubsystem">
|
||||
{% for operation in operations %}
|
||||
<li>
|
||||
<a href="?operation={{ operation }}" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">{{ operation }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-4 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Client
|
||||
</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Secret
|
||||
</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Message
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="p-4 text-xs font-medium text-left text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Origin
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="bg-white divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700"
|
||||
>
|
||||
{% for entry in entries %} {% include 'audit/entry.html.j2' %} {%
|
||||
endfor %}
|
||||
<tbody class="bg-white dark:bg-gray-800">
|
||||
|
||||
{% for entry in entries | list %}
|
||||
<tr
|
||||
class="{{ loop.cycle('', 'bg-gray-50 dark:bg-gray-700 ') }}hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
id="entry-{{ entry.id }}"
|
||||
>
|
||||
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{{ entry.timestamp }}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{{ entry.subsystem }}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{{ entry.operation }}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
|
||||
{% if entry.client_name %}
|
||||
<abbr title="{{ entry.client_id }}">{{ entry.client_name }}</abbr>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{% if entry.secret_name %}
|
||||
<abbr title="{{ entry.secret_id }}">{{ entry.secret_name }}</abbr>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td
|
||||
class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400"
|
||||
>
|
||||
{{ entry.message }}
|
||||
</td>
|
||||
<td
|
||||
class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400"
|
||||
>
|
||||
{{ entry.origin }}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@ -6,7 +6,11 @@
|
||||
|
||||
<span class="text-sm font-normal text-gray-500 dark:text-gray-400"
|
||||
>Showing
|
||||
{% if page_info.total < page_info.last %}
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{page_info.first }}-{{ page_info.total}}</span> of
|
||||
{% else %}
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{page_info.first }}-{{ page_info.last}}</span> of
|
||||
{% endif %}
|
||||
<span class="font-semibold text-gray-900 dark:text-white"
|
||||
>{{ page_info.total }}</span
|
||||
></span
|
||||
|
||||
@ -10,80 +10,124 @@
|
||||
<div class="grid w-full grid-cols-1 gap-4 mt-4 xl:grid-cols-2 2xl:grid-cols-3">
|
||||
<div class="items-center justify-between p-4 bg-white border border-gray-200 rounded-lg shadow-sm sm:flex dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="w-full">
|
||||
<h3 class="text-base font-normal text-gray-500 dark:text-gray-400">Clients</h3>
|
||||
<span class="text-2xl font-bold leading-none text-gray-900 sm:text-3xl dark:text-white">{{ stats.clients }}</span>
|
||||
<h3 class="text-xl font-bold text-gray-500 dark:text-gray-400">Stats</h3>
|
||||
<dl class="max-w-md text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700">
|
||||
<div class="flex flex-col pb-3">
|
||||
<dt class="mb-1 text-gray-500 md:text-lg dark:text-gray-400">Clients</dt>
|
||||
<dd class="text-lg font-semibold">{{ stats.clients }}</dd>
|
||||
</div>
|
||||
<div class="flex flex-col py-3">
|
||||
<dt class="mb-1 text-gray-500 md:text-lg dark:text-gray-400">Secrets</dt>
|
||||
<dd class="text-lg font-semibold">{{ stats.secrets }}</dd>
|
||||
</div>
|
||||
<div class="flex flex-col py-3">
|
||||
<dt class="mb-1 text-gray-500 md:text-lg dark:text-gray-400">Audit Events</dt>
|
||||
<dd class="text-lg font-semibold">{{ stats.audit_events }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="flex mt-4 md:mt-6">
|
||||
<button id="createClientButton" class="text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-3 py-2.5 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800" type="button" data-drawer-target="drawer-create-client-default" data-drawer-show="drawer-create-client-default" aria-controls="drawer-create-client-default" data-drawer-placement="right">
|
||||
Add new client
|
||||
</button>
|
||||
<button id="createSecretButton" class="text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-3 py-2.5 ms-2 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800" type="button" data-drawer-target="drawer-create-secret-default" data-drawer-show="drawer-create-secret-default" aria-controls="drawer-create-secret-default" data-drawer-placement="right">
|
||||
Add new secret
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="items-center 2xl: col-span-2 xl:col-span-2 justify-between p-4 bg-white border border-gray-200 rounded-lg shadow-sm sm:flex dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="w-full">
|
||||
<h3 class="text-base font-normal text-gray-500 dark:text-gray-400">Last Login Events</h3>
|
||||
{% if last_login_events.total > 0 %}
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Timestamp</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Subsystem</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Client/Username</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Origin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800">
|
||||
{% for entry in last_login_events.results | list %}
|
||||
<tr
|
||||
class="{{ loop.cycle('', 'bg-gray-50 dark:bg-gray-700 ') }}hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
id="login-entry-{{ entry.id }}"
|
||||
>
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{{ entry.timestamp }}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{{ entry.subsystem }}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{% if entry.client_name %}
|
||||
{{ entry.client_name }}
|
||||
{% elif entry.data.username %}
|
||||
{{ entry.data.username }}
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{{ entry.origin }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-sm italic">No entries</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="items-center 2xl:col-span-3 xl:col-span-3 justify-between p-4 bg-white border border-gray-200 rounded-lg shadow-sm sm:flex dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="w-full">
|
||||
<h3 class="text-base font-normal text-gray-500 dark:text-gray-400">Last Audit Events</h3>
|
||||
{% if last_audit_events.total > 0 %}
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Timestamp</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Subsystem</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Message</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Origin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800">
|
||||
{% for entry in last_audit_events.results | list %}
|
||||
<tr
|
||||
class="{{ loop.cycle('', 'bg-gray-50 dark:bg-gray-700 ') }}hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
id="login-entry-{{ entry.id }}"
|
||||
>
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{{ entry.timestamp }}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{{ entry.subsystem }}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{{ entry.message }}
|
||||
</td>
|
||||
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
{{ entry.origin }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-sm italic">No entries</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- reference -->
|
||||
|
||||
<div class="grid w-full grid-cols-1 gap-4 mt-4 xl:grid-cols-2 2xl:grid-cols-3">
|
||||
<div class="items-center justify-between p-4 bg-white border border-gray-200 rounded-lg shadow-sm sm:flex dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="w-full">
|
||||
<h3 class="text-base font-normal text-gray-500 dark:text-gray-400">New products</h3>
|
||||
<span class="text-2xl font-bold leading-none text-gray-900 sm:text-3xl dark:text-white">2,340</span>
|
||||
<p class="flex items-center text-base font-normal text-gray-500 dark:text-gray-400">
|
||||
<span class="flex items-center mr-1.5 text-sm text-green-500 dark:text-green-400">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path clip-rule="evenodd" fill-rule="evenodd" d="M10 17a.75.75 0 01-.75-.75V5.612L5.29 9.77a.75.75 0 01-1.08-1.04l5.25-5.5a.75.75 0 011.08 0l5.25 5.5a.75.75 0 11-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0110 17z"></path>
|
||||
</svg>
|
||||
12.5%
|
||||
</span>
|
||||
Since last month
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-full" id="new-products-chart"></div>
|
||||
</div>
|
||||
<div class="items-center justify-between p-4 bg-white border border-gray-200 rounded-lg shadow-sm sm:flex dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="w-full">
|
||||
<h3 class="text-base font-normal text-gray-500 dark:text-gray-400">Users</h3>
|
||||
<span class="text-2xl font-bold leading-none text-gray-900 sm:text-3xl dark:text-white">2,340</span>
|
||||
<p class="flex items-center text-base font-normal text-gray-500 dark:text-gray-400">
|
||||
<span class="flex items-center mr-1.5 text-sm text-green-500 dark:text-green-400">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path clip-rule="evenodd" fill-rule="evenodd" d="M10 17a.75.75 0 01-.75-.75V5.612L5.29 9.77a.75.75 0 01-1.08-1.04l5.25-5.5a.75.75 0 011.08 0l5.25 5.5a.75.75 0 11-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0110 17z"></path>
|
||||
</svg>
|
||||
3,4%
|
||||
</span>
|
||||
Since last month
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-full" id="week-signups-chart"></div>
|
||||
</div>
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="w-full">
|
||||
<h3 class="mb-2 text-base font-normal text-gray-500 dark:text-gray-400">Audience by age</h3>
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="w-16 text-sm font-medium dark:text-white">50+</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
|
||||
<div class="bg-primary-600 h-2.5 rounded-full dark:bg-primary-500" style="width: 18%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="w-16 text-sm font-medium dark:text-white">40+</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
|
||||
<div class="bg-primary-600 h-2.5 rounded-full dark:bg-primary-500" style="width: 15%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="w-16 text-sm font-medium dark:text-white">30+</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
|
||||
<div class="bg-primary-600 h-2.5 rounded-full dark:bg-primary-500" style="width: 60%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="w-16 text-sm font-medium dark:text-white">20+</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
|
||||
<div class="bg-primary-600 h-2.5 rounded-full dark:bg-primary-500" style="width: 30%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="traffic-channels-chart" class="w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{% include '/clients/drawer_client_create.html.j2' %}
|
||||
{% include '/secrets/drawer_secret_create.html.j2' %}
|
||||
{% endblock %}
|
||||
|
||||
@ -3,10 +3,12 @@
|
||||
# pyright: reportUnusedFunction=false
|
||||
import logging
|
||||
import math
|
||||
from typing import Annotated
|
||||
from typing import Annotated, cast
|
||||
from fastapi import APIRouter, Depends, Request, Response
|
||||
from pydantic import BaseModel
|
||||
|
||||
from sshecret.backend import AuditFilter, Operation
|
||||
|
||||
from sshecret_admin.auth import User
|
||||
from sshecret_admin.services import AdminBackend
|
||||
|
||||
@ -45,28 +47,33 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
templates = dependencies.templates
|
||||
|
||||
async def resolve_audit_entries(
|
||||
request: Request, current_user: User, admin: AdminBackend, page: int
|
||||
request: Request,
|
||||
current_user: User,
|
||||
admin: AdminBackend,
|
||||
page: int,
|
||||
filters: AuditFilter,
|
||||
) -> Response:
|
||||
"""Resolve audit entries."""
|
||||
LOG.info("Page: %r", page)
|
||||
total_messages = await admin.get_audit_log_count()
|
||||
per_page = 20
|
||||
offset = 0
|
||||
if page > 1:
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
entries = await admin.get_audit_log(offset=offset, limit=per_page)
|
||||
LOG.info("Entries: %r", entries)
|
||||
filter_args = cast(dict[str, str], filters.model_dump(exclude_none=True))
|
||||
audit_log = await admin.get_audit_log_detailed(offset, per_page, **filter_args)
|
||||
page_info = PagingInfo(
|
||||
page=page, limit=per_page, total=total_messages, offset=offset
|
||||
page=page, limit=per_page, total=audit_log.total, offset=offset
|
||||
)
|
||||
operations = list(Operation)
|
||||
if request.headers.get("HX-Request"):
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"audit/inner.html.j2",
|
||||
{
|
||||
"entries": entries,
|
||||
"entries": audit_log.results,
|
||||
"page_info": page_info,
|
||||
"operations": operations,
|
||||
},
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
@ -74,9 +81,10 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
"audit/index.html.j2",
|
||||
{
|
||||
"page_title": "Audit",
|
||||
"entries": entries,
|
||||
"entries": audit_log.results,
|
||||
"user": current_user.username,
|
||||
"page_info": page_info,
|
||||
"operations": operations,
|
||||
},
|
||||
)
|
||||
|
||||
@ -85,19 +93,21 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
request: Request,
|
||||
current_user: Annotated[User, Depends(dependencies.get_user_from_access_token)],
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
filters: Annotated[AuditFilter, Depends()],
|
||||
) -> Response:
|
||||
"""Get audit entries."""
|
||||
return await resolve_audit_entries(request, current_user, admin, 1)
|
||||
return await resolve_audit_entries(request, current_user, admin, 1, filters)
|
||||
|
||||
@app.get("/audit/page/{page}")
|
||||
async def get_audit_entries_page(
|
||||
request: Request,
|
||||
current_user: Annotated[User, Depends(dependencies.get_user_from_access_token)],
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
filters: Annotated[AuditFilter, Depends()],
|
||||
page: int,
|
||||
) -> Response:
|
||||
"""Get audit entries."""
|
||||
LOG.info("Get audit entries page: %r", page)
|
||||
return await resolve_audit_entries(request, current_user, admin, page)
|
||||
return await resolve_audit_entries(request, current_user, admin, page, filters)
|
||||
|
||||
return app
|
||||
|
||||
@ -8,6 +8,7 @@ from fastapi import APIRouter, Depends, Query, Request, Response, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlmodel import Session
|
||||
from sshecret_admin.services import AdminBackend
|
||||
from starlette.datastructures import URL
|
||||
|
||||
from sshecret_admin.auth import (
|
||||
@ -17,6 +18,8 @@ from sshecret_admin.auth import (
|
||||
create_refresh_token,
|
||||
)
|
||||
|
||||
from sshecret.backend.models import Operation
|
||||
|
||||
from ..dependencies import FrontendDependencies
|
||||
from ..exceptions import RedirectException
|
||||
|
||||
@ -30,6 +33,19 @@ class LoginError(BaseModel):
|
||||
message: str
|
||||
|
||||
|
||||
async def audit_login_failure(admin: AdminBackend, username: str, request: Request) -> None:
|
||||
"""Write login failure to audit log."""
|
||||
origin: str | None = None
|
||||
if request.client:
|
||||
origin = request.client.host
|
||||
await admin.write_audit_message(
|
||||
operation=Operation.DENY,
|
||||
message="Login failed",
|
||||
origin=origin or "UNKNOWN",
|
||||
username=username,
|
||||
)
|
||||
|
||||
|
||||
def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
"""Create auth router."""
|
||||
|
||||
@ -64,6 +80,7 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
request: Request,
|
||||
response: Response,
|
||||
session: Annotated[Session, Depends(dependencies.get_db_session)],
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
|
||||
next: Annotated[str, Query()] = "/dashboard",
|
||||
error_title: str | None = None,
|
||||
@ -89,6 +106,7 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
)
|
||||
)
|
||||
if not user:
|
||||
await audit_login_failure(admin, form_data.username, request)
|
||||
raise login_failed
|
||||
token_data: dict[str, str] = {"sub": user.username}
|
||||
access_token = create_access_token(dependencies.settings, data=token_data)
|
||||
@ -108,6 +126,17 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
secure=False,
|
||||
samesite="strict",
|
||||
)
|
||||
origin = "UNKNOWN"
|
||||
if request.client:
|
||||
origin = request.client.host
|
||||
|
||||
await admin.write_audit_message(
|
||||
operation=Operation.LOGIN,
|
||||
message="Logged in to admin frontend",
|
||||
origin=origin,
|
||||
username=form_data.username,
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
@app.get("/refresh")
|
||||
|
||||
@ -56,6 +56,9 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
):
|
||||
"""Dashboard for mocking up the dashboard."""
|
||||
stats = await get_stats(admin)
|
||||
last_login_events = await admin.get_audit_log_detailed(limit=5, operation="login")
|
||||
last_audit_events = await admin.get_audit_log_detailed(limit=10)
|
||||
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
@ -64,6 +67,9 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
"page_title": "sshecret",
|
||||
"user": current_user.username,
|
||||
"stats": stats,
|
||||
"last_login_events": last_login_events,
|
||||
"last_audit_events": last_audit_events,
|
||||
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user