Improve logging, display additional info to superadmin

This commit is contained in:
Matteo Gheza 2024-01-09 22:05:45 +01:00
parent 2e35f10b97
commit 350e0b22bd
10 changed files with 273 additions and 40 deletions

View File

@ -15,12 +15,28 @@ class LogsController extends Controller
{
User::where('id', $request->user()->id)->update(['last_access' => now()]);
$query = Log::join('users as changed_user', 'changed_user.id', '=', 'logs.changed_id')
->join('users as editor_user', 'editor_user.id', '=', 'logs.editor_id')
->orderBy('created_at', 'desc');
$selectedCols = [
"logs.id", "logs.action", "logs.editor_id", "logs.changed_id", "logs.created_at", "logs.source_type",
"changed_user.name as changed", "editor_user.name as editor", "editor_user.hidden as editor_hidden"
];
if($request->user()->hasPermission("logs-limited-read")) {
$query = $query->where(function ($query) {
$query->where('editor_user.hidden', false)
->orWhere('editor_user.id', auth()->user()->id);
});
} else if($request->user()->hasPermission("logs-read")) {
$selectedCols = array_merge($selectedCols, ["logs.ip", "logs.user_agent"]);
} else {
abort(401);
}
return response()->json(
Log::join('users as changed_user', 'changed_user.id', '=', 'logs.changed_id')
->join('users as editor_user', 'editor_user.id', '=', 'logs.editor_id')
->select("logs.id", "logs.action", "logs.editor_id", "logs.changed_id", "logs.created_at", "changed_user.name as changed", "editor_user.name as editor")
->orderBy('created_at', 'desc')
->get()
$query->select($selectedCols)->get()
);
}
}

View File

@ -16,9 +16,6 @@ class Logger {
if(is_null($editor)) $editor = auth()->user();
$log->editor()->associate($editor);
//Check if editor has attribute hidden
if($editor->hidden) return;
$request = request();
if($source_type !== "web") {
$log->ip = null;

View File

@ -18,6 +18,7 @@ return [
'services' => 'c,r,u,d',
'trainings' => 'c,r,u,d',
'alerts' => 'c,r,u',
'logs' => 'r'
],
'admin' => [
'users' => 'c,r,u,d,i,b,h,sc,sd,atc,ame',
@ -25,6 +26,7 @@ return [
'services' => 'c,r,u,d',
'trainings' => 'c,r,u,d',
'alerts' => 'c,r,u',
'logs' => 'lr'
],
'chief' => [
'users' => 'r,u,sc,sd,atc,ame',
@ -32,6 +34,7 @@ return [
'services' => 'c,r,u,d',
'trainings' => 'c,r,u,d',
'alerts' => 'c,r,u',
'logs' => 'lr'
],
'user' => [
'users' => 'lr',
@ -39,6 +42,7 @@ return [
'services' => 'c,r,u,d',
'trainings' => 'c,r,u,d',
'alerts' => 'r',
'logs' => 'lr'
]
],

View File

@ -34,6 +34,7 @@
"rxjs": "~7.4.0",
"sweetalert2": "^11.3.4",
"tslib": "^2.3.0",
"ua-parser-js": "^1.0.37",
"zone.js": "~0.14.2"
},
"devDependencies": {
@ -44,6 +45,7 @@
"@types/jasmine": "~3.10.0",
"@types/leaflet": "^1.7.8",
"@types/node": "^20.10.5",
"@types/ua-parser-js": "^0.7.39",
"jasmine-core": "4.0.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
@ -4079,6 +4081,12 @@
"@types/node": "*"
}
},
"node_modules/@types/ua-parser-js": {
"version": "0.7.39",
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz",
"integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==",
"dev": true
},
"node_modules/@types/ws": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
@ -5125,29 +5133,6 @@
"node": ">=8"
}
},
"node_modules/browser-sync/node_modules/ua-parser-js": {
"version": "1.0.37",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz",
"integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
},
{
"type": "github",
"url": "https://github.com/sponsors/faisalman"
}
],
"engines": {
"node": "*"
}
},
"node_modules/browserslist": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
@ -8572,6 +8557,29 @@
"node": ">=8.17.0"
}
},
"node_modules/karma/node_modules/ua-parser-js": {
"version": "0.7.37",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.37.tgz",
"integrity": "sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
},
{
"type": "github",
"url": "https://github.com/sponsors/faisalman"
}
],
"engines": {
"node": "*"
}
},
"node_modules/karma/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
@ -12429,10 +12437,9 @@
}
},
"node_modules/ua-parser-js": {
"version": "0.7.35",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz",
"integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==",
"dev": true,
"version": "1.0.37",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz",
"integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==",
"funding": [
{
"type": "opencollective",
@ -12441,6 +12448,10 @@
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
},
{
"type": "github",
"url": "https://github.com/sponsors/faisalman"
}
],
"engines": {

View File

@ -39,6 +39,7 @@
"rxjs": "~7.4.0",
"sweetalert2": "^11.3.4",
"tslib": "^2.3.0",
"ua-parser-js": "^1.0.37",
"zone.js": "~0.14.2"
},
"devDependencies": {
@ -49,6 +50,7 @@
"@types/jasmine": "~3.10.0",
"@types/leaflet": "^1.7.8",
"@types/node": "^20.10.5",
"@types/ua-parser-js": "^0.7.39",
"jasmine-core": "4.0.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",

View File

@ -64,14 +64,36 @@
<th>{{ 'changed'|translate|ftitlecase }}</th>
<th>{{ 'editor'|translate|ftitlecase }}</th>
<th>{{ 'datetime'|translate|ftitlecase }}</th>
<th *ngIf="auth.profile.can('logs-read')">{{ 'device_information'|translate|ftitlecase }}</th>
</tr>
</thead>
<tbody id="table_body">
<tr *ngFor="let row of displayedData; index as i">
<td>{{ row.action }}</td>
<td>
{{ row.action }}
<div class="float-end d-inline">
<i *ngIf="row.source_type === 'telegram'" class="fab faxlarge fa-telegram"></i>
<i *ngIf="row.source_type === 'web'" class="fa faxlarge fa-globe"></i>
</div>
</td>
<td>{{ row.changed }}</td>
<td>{{ row.editor }}</td>
<td>
<div class="float-start d-inline" *ngIf="row.editor_hidden">
<i class="fa fa-ghost"></i>
</div>
{{ row.editor }}
</td>
<td>{{ row.created_at | date: 'dd/MM/YYYY HH:mm:ss' }}</td>
<td *ngIf="auth.profile.can('logs-read') && row.source_type === 'web'">
<a [href]="'https://iplocation.io/ip/'+row.ip" target="_blank" *ngIf="isPublicIp(row.ip)">
<code>{{ row.ip }}</code>
</a>
<code *ngIf="!isPublicIp(row.ip)">{{ row.ip }}</code>
<div class="mt-1" (click)="openUserAgentInfoModal(row.user_agent, useragentInfoModal)">
<i *ngFor="let icon of userAgentToIcons(row.user_agent)" [ngClass]="'m-1 '+icon"></i>
</div>
</td>
<td *ngIf="auth.profile.can('logs-read') && row.source_type !== 'web'"></td>
</tr>
</tbody>
</table>
@ -192,3 +214,48 @@
<ng-template #firstTemplate let-disabled="disabled" let-currentPage="currentPage">
{{ 'first'|translate|ftitlecase }}
</ng-template>
<ng-template #useragentInfoModal>
<div class="modal-header">
<h4 class="modal-title pull-left" translate>{{ 'useragent_info_modal.title' }}</h4>
<button type="button" class="btn-close close pull-right" aria-label="Close" (click)="useragentInfoModalRef?.hide()">
<span aria-hidden="true" class="visually-hidden">&times;</span>
</button>
</div>
<div class="modal-body">
<table class="table table-responsive">
<thead>
<tr>
<th>{{ 'property'|translate|ftitlecase }}</th>
<th>{{ 'value'|translate|ftitlecase }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ 'user_agent'|translate|ftitlecase }}</td>
<td><code>{{ processedUA.ua }}</code></td>
</tr>
<tr *ngIf="processedUA.browser && processedUA.browser.name">
<td>{{ 'browser'|translate|ftitlecase }}</td>
<td><code>{{ processedUA.browser.name }} {{ processedUA.browser.version }}</code></td>
</tr>
<tr *ngIf="processedUA.engine && processedUA.engine.name">
<td>{{ 'engine'|translate|ftitlecase }}</td>
<td><code>{{ processedUA.engine.name }} {{ processedUA.engine.version }}</code></td>
</tr>
<tr *ngIf="processedUA.os && processedUA.os.name">
<td>{{ 'os'|translate|ftitlecase }}</td>
<td><code>{{ processedUA.os.name }} {{ processedUA.os.version }}</code></td>
</tr>
<tr *ngIf="processedUA.device && processedUA.device.name">
<td>{{ 'device'|translate|ftitlecase }}</td>
<td><code>{{ processedUA.device.vendor }} {{ processedUA.device.model }}</code></td>
</tr>
<tr *ngIf="processedUA.cpu && processedUA.cpu.architecture">
<td>{{ 'cpu'|translate|ftitlecase }}</td>
<td><code>{{ processedUA.cpu.architecture }}</code></td>
</tr>
</tbody>
</table>
</div>
</ng-template>

View File

@ -1,11 +1,13 @@
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, TemplateRef } from '@angular/core';
import { Router } from '@angular/router';
import { ApiClientService } from 'src/app/_services/api-client.service';
import { AuthService } from '../../_services/auth.service';
import { ToastrService } from 'ngx-toastr';
import { PageChangedEvent } from 'ngx-bootstrap/pagination';
import { TranslateService } from '@ngx-translate/core';
import Swal from 'sweetalert2';
import { PageChangedEvent } from 'ngx-bootstrap/pagination';
import * as UAParser from 'ua-parser-js';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
@Component({
selector: 'app-table',
@ -78,12 +80,16 @@ export class TableComponent implements OnInit, OnDestroy {
public searchText: string = "";
public searchData: any = [];
public useragentInfoModalRef: BsModalRef | undefined;
public processedUA: any = {};
constructor(
private api: ApiClientService,
public auth: AuthService,
private router: Router,
private toastr: ToastrService,
private translate: TranslateService
private translate: TranslateService,
private modalService: BsModalService
) { }
loadTableData() {
@ -281,4 +287,94 @@ export class TableComponent implements OnInit, OnDestroy {
extractNamesFromObject(obj: any) {
return obj.flatMap((e: any) => e.name);
}
userAgentToIcons(userAgentString: string) {
const parser = new UAParser(userAgentString);
let icons = [];
switch (parser.getBrowser().name) {
case 'Chrome':
case 'Chromium':
case 'Chrome WebView':
case 'Chrome Headless':
icons.push('fab fa-chrome');
break;
case 'Mozilla':
case 'Firefox [Focus/Reality]':
icons.push('fab fa-firefox-browser');
break;
case 'Safari':
icons.push('fab fa-safari');
break;
case 'IE':
case 'IEMobile':
icons.push('fa fa-skull-crossbones');
break;
case 'Edge':
icons.push('fab fa-edge');
break;
case 'Android Browser':
case 'Huawei Browser':
case 'Samsung Browser':
icons.push('fab fa-android');
break;
case 'Silk':
icons.push('fab fa-amazon');
break;
case 'Instagram':
case 'TikTok':
case 'Snapchat':
case 'Facebook':
case 'WeChat':
icons.push('fa fa-square-share-nodes');
break;
case 'Electron':
case 'PhantomJS':
icons.push('fa fa-code');
break;
default:
icons.push('fa fa-question-circle');
}
switch (parser.getDevice().type) {
case 'mobile':
icons.push('fa fa-mobile');
break;
case 'tablet':
icons.push('fa fa-tablet');
break;
case 'smarttv':
icons.push('fa fa-tv');
break;
case 'console':
icons.push('fa fa-gamepad');
break;
case 'wearable':
icons.push('fa fa-watch');
break;
default:
icons.push('fa fa-desktop');
}
console.log(parser.getResult(), icons);
return icons;
}
isPublicIp(ipAddress: string) {
const parts = ipAddress.split('.');
if (parts.length === 4) {
return !(
parts[0] === '10' ||
(parts[0] === '172' && parseInt(parts[1], 10) >= 16 && parseInt(parts[1], 10) <= 31) ||
(parts[0] === '192' && parts[1] === '168') ||
ipAddress === '127.0.0.1'
);
}
return false;
}
openUserAgentInfoModal(userAgentString: string, template: TemplateRef<void>) {
this.processedUA = new UAParser(userAgentString).getResult();
this.useragentInfoModalRef = this.modalService.show(template);
}
}

View File

@ -116,6 +116,9 @@
"medical_examination_modal": {
"title": "Add medical examination"
},
"useragent_info_modal": {
"title": "Client information"
},
"validation": {
"place_min_length": "Place name must be at least 3 characters long",
"type_must_be_two_characters_long": "Type must be at least 2 characters long",
@ -124,6 +127,14 @@
"document_format_not_supported": "Document format not supported",
"file_too_big": "File too big"
},
"property": "property",
"value": "value",
"user_agent": "User Agent",
"browser": "browser",
"engine": "engine",
"os": "Operating System",
"device": "device",
"cpu": "CPU",
"username": "username",
"password": "password",
"warning": "warning",
@ -163,6 +174,7 @@
"personal_information": "personal information",
"contact_information": "contact information",
"service_information": "service information",
"device_information": "device information",
"course_date": "course date",
"documents": "documents",
"driving_license": "driving license",

View File

@ -117,12 +117,23 @@
"medical_examination_modal": {
"title": "Aggiungi visita medica"
},
"useragent_info_modal": {
"title": "Informazioni sul client"
},
"validation": {
"place_min_length": "Il nome della località deve essere di almeno 3 caratteri",
"type_must_be_two_characters_long": "La tipologia deve essere di almeno 2 caratteri",
"image_format_not_supported": "Formato immagine non supportato",
"file_too_big": "File troppo grande"
},
"property": "proprietà",
"value": "valore",
"user_agent": "User Agent",
"browser": "browser",
"engine": "motore",
"os": "Sistema Operativo",
"device": "dispositivo",
"cpu": "CPU",
"username": "username",
"password": "password",
"warning": "attenzione",
@ -162,6 +173,7 @@
"personal_information": "anagrafica personale",
"contact_information": "recapiti",
"service_information": "informazioni di servizio",
"device_information": "informazioni sul dispositivo",
"course_date": "data corso",
"documents": "documenti",
"driving_license": "patente",

View File

@ -14,6 +14,14 @@
font-size: 20px;
}
.faxlarge {
font-size: x-large;
}
.fa-telegram {
color: #0088cc;
}
.fa-whatsapp {
color: green;
font-size: 1.5em;
@ -22,3 +30,11 @@
.leaflet-pane.leaflet-shadow-pane {
display: none;
}
a code {
color: var(--bs-code-color) !important;
}
a:has(code) {
text-decoration: none !important;
}