mirror of
https://github.com/jbcr/core.git
synced 2026-03-31 21:32:09 +02:00
Merge pull request #827 from bolt/feature/logging
Feature: Add logs, including Log Viewer screen
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
monolog:
|
||||
channels: ['db']
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
@@ -14,6 +15,10 @@ monolog:
|
||||
# type: chromephp
|
||||
# level: info
|
||||
console:
|
||||
type: console
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ['!event', '!doctrine', '!console']
|
||||
db:
|
||||
channels: ['db']
|
||||
type: service
|
||||
id: Bolt\Log\LogHandler
|
||||
@@ -1,4 +1,5 @@
|
||||
monolog:
|
||||
channels: ['db']
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
@@ -10,6 +11,10 @@ monolog:
|
||||
path: '%kernel.logs_dir%/%kernel.environment%.log'
|
||||
level: debug
|
||||
console:
|
||||
type: console
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ['!event', '!doctrine']
|
||||
db:
|
||||
channels: ['db']
|
||||
type: service
|
||||
id: Bolt\Log\LogHandler
|
||||
|
||||
7
config/packages/test/monolog.yaml
Normal file
7
config/packages/test/monolog.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
monolog:
|
||||
channels: ['db']
|
||||
handlers:
|
||||
db:
|
||||
channels: ['db']
|
||||
type: service
|
||||
id: Bolt\Log\LogHandler
|
||||
@@ -69,7 +69,7 @@ services:
|
||||
Bolt\Menu\BackendMenuBuilderInterface: '@Bolt\Menu\BackendMenuBuilder'
|
||||
|
||||
Bolt\Menu\FrontendMenuBuilder: ~
|
||||
|
||||
|
||||
Bolt\Menu\FrontendMenuBuilderInterface: '@Bolt\Menu\FrontendMenuBuilder'
|
||||
|
||||
# Needed for SetContent from bolt/core
|
||||
@@ -87,5 +87,10 @@ services:
|
||||
|
||||
Doctrine\ORM\Query\Expr: ~
|
||||
|
||||
monolog.processor.request:
|
||||
class: Bolt\Log\RequestProcessor
|
||||
tags:
|
||||
- { name: monolog.processor, method: processRecord, handler: db }
|
||||
|
||||
Twig\Extension\StringLoaderExtension: ~
|
||||
|
||||
|
||||
34
src/Controller/Backend/LogViewerController.php
Normal file
34
src/Controller/Backend/LogViewerController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Controller\Backend;
|
||||
|
||||
use Bolt\Controller\TwigAwareController;
|
||||
use Bolt\Repository\LogRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Security("is_granted('ROLE_ADMIN')")
|
||||
*/
|
||||
class LogViewerController extends TwigAwareController implements BackendZone
|
||||
{
|
||||
/**
|
||||
* @Route("/logviewer", name="bolt_logviewer", methods={"GET"})
|
||||
*/
|
||||
public function index(LogRepository $log, Request $request): Response
|
||||
{
|
||||
$amount = $this->config->get('general/log/amount', 10);
|
||||
$page = (int) $request->get('page', 1);
|
||||
|
||||
/** @var Log $items */
|
||||
$items = $log->findLatest($page, $amount);
|
||||
|
||||
return $this->renderTemplate('@bolt/pages/logviewer.html.twig', [
|
||||
'items' => $items,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ namespace Bolt\Controller\Frontend;
|
||||
use Bolt\Controller\TwigAwareController;
|
||||
use Bolt\Repository\ContentRepository;
|
||||
use Bolt\TemplateChooser;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
@@ -15,9 +16,13 @@ class HomepageController extends TwigAwareController implements FrontendZone
|
||||
/** @var TemplateChooser */
|
||||
private $templateChooser;
|
||||
|
||||
public function __construct(TemplateChooser $templateChooser)
|
||||
/** @var LoggerInterface */
|
||||
private $logger;
|
||||
|
||||
public function __construct(TemplateChooser $templateChooser, LoggerInterface $dbLogger)
|
||||
{
|
||||
$this->templateChooser = $templateChooser;
|
||||
$this->logger = $dbLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,6 +45,8 @@ class HomepageController extends TwigAwareController implements FrontendZone
|
||||
|
||||
$templates = $this->templateChooser->forHomepage();
|
||||
|
||||
$this->logger->notice('Huius, Lyco, oratione locuples, rebus ipsis ielunior. Quid autem habent admirationis, cum prope accesseris?!', ['foo' => 'bar']);
|
||||
|
||||
return $this->renderTemplate($templates, ['record' => $record]);
|
||||
}
|
||||
}
|
||||
|
||||
169
src/Entity/Log.php
Normal file
169
src/Entity/Log.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass="AppBundle\Repository\LogRepository")
|
||||
* @ORM\Table(name="log")
|
||||
* @ORM\HasLifecycleCallbacks
|
||||
*/
|
||||
class Log
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue(strategy="AUTO")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="message", type="text")
|
||||
*/
|
||||
private $message;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="context", type="array", nullable=true)
|
||||
*/
|
||||
private $context;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="level", type="smallint")
|
||||
*/
|
||||
private $level;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="level_name", type="string", length=50)
|
||||
*/
|
||||
private $levelName;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="created_at", type="datetime")
|
||||
*/
|
||||
private $createdAt;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="extra", type="array", nullable=true)
|
||||
*/
|
||||
private $extra;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="user", type="array", nullable=true)
|
||||
*/
|
||||
private $user;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="location", type="array", nullable=true)
|
||||
*/
|
||||
private $location;
|
||||
|
||||
/**
|
||||
* @ORM\PrePersist
|
||||
*/
|
||||
public function onPrePersist(): void
|
||||
{
|
||||
$this->createdAt = new \DateTime();
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(int $id): self
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function setMessage(string $message): self
|
||||
{
|
||||
$this->message = $message;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContext(): ?array
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
public function setContext(?array $context): self
|
||||
{
|
||||
$this->context = $context;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLevel(): int
|
||||
{
|
||||
return $this->level;
|
||||
}
|
||||
|
||||
public function setLevel(int $level): self
|
||||
{
|
||||
$this->level = $level;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLevelName(): string
|
||||
{
|
||||
return $this->levelName;
|
||||
}
|
||||
|
||||
public function setLevelName(string $levelName): self
|
||||
{
|
||||
$this->levelName = $levelName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getExtra(): ?array
|
||||
{
|
||||
return $this->extra;
|
||||
}
|
||||
|
||||
public function setExtra(?array $extra): self
|
||||
{
|
||||
$this->extra = $extra;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): \DateTime
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(\DateTime $createdAt): self
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLocation(): ?array
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
public function setLocation(?array $location): self
|
||||
{
|
||||
$this->location = $location;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUser(): ?array
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(?array $user): self
|
||||
{
|
||||
$this->user = $user;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
41
src/Log/LogHandler.php
Normal file
41
src/Log/LogHandler.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Log;
|
||||
|
||||
use Bolt\Entity\Log;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Monolog\Handler\AbstractProcessingHandler;
|
||||
|
||||
class LogHandler extends AbstractProcessingHandler
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when writing to our database
|
||||
*/
|
||||
protected function write(array $record): void
|
||||
{
|
||||
$logEntry = new Log();
|
||||
$logEntry->setMessage($record['message']);
|
||||
$logEntry->setLevel($record['level']);
|
||||
$logEntry->setLevelName($record['level_name']);
|
||||
$logEntry->setExtra($record['extra']);
|
||||
$logEntry->setUser($record['user'] ?? null);
|
||||
$logEntry->setLocation($record['location']);
|
||||
$logEntry->setContext($record['context']);
|
||||
|
||||
$this->em->persist($logEntry);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
67
src/Log/RequestProcessor.php
Normal file
67
src/Log/RequestProcessor.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Log;
|
||||
|
||||
use Bolt\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Webmozart\PathUtil\Path;
|
||||
|
||||
class RequestProcessor
|
||||
{
|
||||
/** @var RequestStack */
|
||||
protected $request;
|
||||
|
||||
/** @var Security */
|
||||
private $security;
|
||||
|
||||
/** @var string */
|
||||
private $projectDir;
|
||||
|
||||
public function __construct(RequestStack $request, Security $security, KernelInterface $kernel)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->security = $security;
|
||||
$this->projectDir = $kernel->getProjectDir();
|
||||
}
|
||||
|
||||
public function processRecord(array $record): array
|
||||
{
|
||||
$req = $this->request->getCurrentRequest();
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->security->getUser();
|
||||
|
||||
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 7);
|
||||
|
||||
$record['extra'] = [
|
||||
'client_ip' => $req->getClientIp(),
|
||||
'client_port' => $req->getPort(),
|
||||
'uri' => $req->getUri(),
|
||||
'query_string' => $req->getQueryString(),
|
||||
'method' => $req->getMethod(),
|
||||
'request' => $req->request->all(),
|
||||
];
|
||||
|
||||
if (! empty($user)) {
|
||||
$record['user'] = [
|
||||
'id' => $user->getId(),
|
||||
'username' => $user->getUsername(),
|
||||
'roles' => $user->getRoles(),
|
||||
];
|
||||
}
|
||||
|
||||
$record['location'] = [
|
||||
'file' => '…/' . Path::makeRelative($trace[5]['file'], $this->projectDir),
|
||||
'line' => $trace[5]['line'],
|
||||
'class' => $trace[6]['class'],
|
||||
'type' => $trace[6]['type'],
|
||||
'function' => $trace[6]['function'],
|
||||
];
|
||||
|
||||
return $record;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use Bolt\Configuration\Config;
|
||||
use Bolt\Configuration\Content\ContentType;
|
||||
use Bolt\Repository\ContentRepository;
|
||||
use Bolt\Twig\ContentExtension;
|
||||
use Bolt\Version;
|
||||
use Knp\Menu\FactoryInterface;
|
||||
use Knp\Menu\ItemInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
@@ -207,6 +208,14 @@ final class BackendMenuBuilder implements BackendMenuBuilderInterface
|
||||
],
|
||||
]);
|
||||
|
||||
$menu->getChild('Maintenance')->addChild('Log viewer', [
|
||||
'uri' => $this->urlGenerator->generate('bolt_logviewer'),
|
||||
'extras' => [
|
||||
'name' => $t->trans('caption.logviewer'),
|
||||
'icon' => 'fa-clipboard',
|
||||
],
|
||||
]);
|
||||
|
||||
$menu->getChild('Maintenance')->addChild('Bolt API', [
|
||||
'uri' => $this->urlGenerator->generate('api_entrypoint'),
|
||||
'extras' => [
|
||||
@@ -215,13 +224,16 @@ final class BackendMenuBuilder implements BackendMenuBuilderInterface
|
||||
],
|
||||
]);
|
||||
|
||||
$menu->getChild('Maintenance')->addChild('Fixtures', [
|
||||
'uri' => '',
|
||||
'extras' => [
|
||||
'name' => $t->trans('caption.fixtures_dummy_content'),
|
||||
'icon' => 'fa-hat-wizard',
|
||||
],
|
||||
]);
|
||||
/*
|
||||
* @todo Make fixtures work from the backend
|
||||
*/
|
||||
// $menu->getChild('Maintenance')->addChild('Fixtures', [
|
||||
// 'uri' => '',
|
||||
// 'extras' => [
|
||||
// 'name' => $t->trans('caption.fixtures_dummy_content'),
|
||||
// 'icon' => 'fa-hat-wizard',
|
||||
// ],
|
||||
// ]);
|
||||
|
||||
$menu->getChild('Maintenance')->addChild('Clear the cache', [
|
||||
'uri' => $this->urlGenerator->generate('bolt_clear_cache'),
|
||||
@@ -231,13 +243,16 @@ final class BackendMenuBuilder implements BackendMenuBuilderInterface
|
||||
],
|
||||
]);
|
||||
|
||||
$menu->getChild('Maintenance')->addChild('Installation checks', [
|
||||
'uri' => '',
|
||||
'extras' => [
|
||||
'name' => $t->trans('caption.installation_checks'),
|
||||
'icon' => 'fa-clipboard-check',
|
||||
],
|
||||
]);
|
||||
/*
|
||||
* @todo Make Installation checks work from the backend
|
||||
*/
|
||||
// $menu->getChild('Maintenance')->addChild('Installation checks', [
|
||||
// 'uri' => '',
|
||||
// 'extras' => [
|
||||
// 'name' => $t->trans('caption.installation_checks'),
|
||||
// 'icon' => 'fa-clipboard-check',
|
||||
// ],
|
||||
// ]);
|
||||
|
||||
$menu->getChild('Maintenance')->addChild('Translations', [
|
||||
'uri' => $this->urlGenerator->generate('translation_index'),
|
||||
@@ -247,14 +262,16 @@ final class BackendMenuBuilder implements BackendMenuBuilderInterface
|
||||
],
|
||||
]);
|
||||
|
||||
// @todo When we're close to stable release, make this less prominent
|
||||
$menu->getChild('Maintenance')->addChild('The Kitchensink', [
|
||||
'uri' => $this->urlGenerator->generate('bolt_kitchensink'),
|
||||
'extras' => [
|
||||
'name' => $t->trans('caption.kitchensink'),
|
||||
'icon' => 'fa-bath',
|
||||
],
|
||||
]);
|
||||
// Hide this menu item, unless we're on a "Git clone" install.
|
||||
if (Version::installType() === 'Git clone') {
|
||||
$menu->getChild('Maintenance')->addChild('The Kitchensink', [
|
||||
'uri' => $this->urlGenerator->generate('bolt_kitchensink'),
|
||||
'extras' => [
|
||||
'name' => $t->trans('caption.kitchensink'),
|
||||
'icon' => 'fa-bath',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$menu->getChild('Maintenance')->addChild('About Bolt', [
|
||||
'uri' => $this->urlGenerator->generate('bolt_about'),
|
||||
|
||||
49
src/Repository/LogRepository.php
Normal file
49
src/Repository/LogRepository.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Repository;
|
||||
|
||||
use Bolt\Entity\Log;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Common\Persistence\ManagerRegistry;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Pagerfanta\Adapter\DoctrineORMAdapter;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
|
||||
/**
|
||||
* @method Log|null find($id, $lockMode = null, $lockVersion = null)
|
||||
* @method Log|null findOneBy(array $criteria, array $orderBy = null)
|
||||
* @method Log[] findAll()
|
||||
* @method Log[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||
*/
|
||||
class LogRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Log::class);
|
||||
}
|
||||
|
||||
public function getQueryBuilder(): QueryBuilder
|
||||
{
|
||||
return $this->createQueryBuilder('log');
|
||||
}
|
||||
|
||||
public function findLatest(int $page = 1, int $amount = 6): Pagerfanta
|
||||
{
|
||||
$qb = $this->getQueryBuilder()
|
||||
->orderBy('log.createdAt', 'DESC')
|
||||
->setMaxResults($amount);
|
||||
|
||||
return $this->createPaginator($qb->getQuery(), $page, $amount);
|
||||
}
|
||||
|
||||
private function createPaginator(Query $query, int $page, int $amountPerPage): Pagerfanta
|
||||
{
|
||||
$paginator = new Pagerfanta(new DoctrineORMAdapter($query, true, true));
|
||||
$paginator->setMaxPerPage($amountPerPage);
|
||||
$paginator->setCurrentPage($page);
|
||||
return $paginator;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use Bolt\Entity\User;
|
||||
use Bolt\Entity\UserAuthToken;
|
||||
use Bolt\Repository\UserRepository;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
@@ -39,13 +40,23 @@ class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
|
||||
/** @var ObjectManager */
|
||||
private $em;
|
||||
|
||||
public function __construct(UserRepository $userRepository, RouterInterface $router, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder, ObjectManager $em)
|
||||
{
|
||||
/** @var LoggerInterface */
|
||||
private $logger;
|
||||
|
||||
public function __construct(
|
||||
UserRepository $userRepository,
|
||||
RouterInterface $router,
|
||||
CsrfTokenManagerInterface $csrfTokenManager,
|
||||
UserPasswordEncoderInterface $passwordEncoder,
|
||||
ObjectManager $em,
|
||||
LoggerInterface $dbLogger
|
||||
) {
|
||||
$this->userRepository = $userRepository;
|
||||
$this->router = $router;
|
||||
$this->csrfTokenManager = $csrfTokenManager;
|
||||
$this->passwordEncoder = $passwordEncoder;
|
||||
$this->em = $em;
|
||||
$this->logger = $dbLogger;
|
||||
}
|
||||
|
||||
protected function getLoginUrl()
|
||||
@@ -114,6 +125,12 @@ class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
|
||||
$userArr = [
|
||||
'id' => $user->getId(),
|
||||
'username' => $user->getUsername(),
|
||||
];
|
||||
$this->logger->notice('User \'{username}\' logged in (manually)', $userArr);
|
||||
|
||||
return new RedirectResponse($request->getSession()->get(
|
||||
'_security.'.$providerKey.'.target_path',
|
||||
$this->router->generate('bolt_dashboard')
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Bolt\Security;
|
||||
|
||||
use Bolt\Entity\User;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
@@ -15,9 +16,13 @@ class LogoutListener implements LogoutHandlerInterface
|
||||
{
|
||||
private $em;
|
||||
|
||||
public function __construct(ObjectManager $em)
|
||||
/** @var LoggerInterface */
|
||||
private $logger;
|
||||
|
||||
public function __construct(ObjectManager $em, LoggerInterface $dbLogger)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->logger = $dbLogger;
|
||||
}
|
||||
|
||||
public function logout(Request $request, Response $response, TokenInterface $token): void
|
||||
@@ -26,6 +31,13 @@ class LogoutListener implements LogoutHandlerInterface
|
||||
if (! $user instanceof User) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userArr = [
|
||||
'id' => $user->getId(),
|
||||
'username' => $user->getUsername(),
|
||||
];
|
||||
$this->logger->notice('User \'{username}\' logged out (manually)', $userArr);
|
||||
|
||||
$this->em->remove($user->getUserAuthToken());
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
97
templates/pages/logviewer.html.twig
Normal file
97
templates/pages/logviewer.html.twig
Normal file
@@ -0,0 +1,97 @@
|
||||
{% extends '@bolt/_base/layout.html.twig' %}
|
||||
{% import '@bolt/_macro/_macro.html.twig' as macro %}
|
||||
|
||||
{% block title %}
|
||||
{{ macro.icon('clipboard-list') }}
|
||||
{{ 'caption.logviewer'|trans }}
|
||||
{% endblock title %}
|
||||
|
||||
{# This 'topsection' gets output _before_ the main form, allowing `dump()`, without breaking Vue #}
|
||||
{% block topsection %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{# The 'main' section is the main contents of the page. Usually this is Vue-ified. #}
|
||||
{% block main %}
|
||||
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th scope="col">{{ __('label.id') }}</th>
|
||||
<th scope="col">{{ __('label.level') }}</th>
|
||||
<th scope="col"></th>
|
||||
<th scope="col"></th>
|
||||
<th scope="col">{{ __('label.message') }}</th>
|
||||
<th scope="col"></th>
|
||||
<th scope="col">{{ __('label.timestamp') }}</th>
|
||||
</tr>
|
||||
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<th scope="row" class="text-nowrap">
|
||||
№ {{ item.id }}
|
||||
</th>
|
||||
<td>
|
||||
{{ item.levelName }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-tertiary btn-sm dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ macro.icon('cog') }} {{ __('label.request') }}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" style="width: 400px; font-size: 80%;">
|
||||
<ul class="pl-4 pr-3 my-0">
|
||||
{% for key, line in item.extra %}
|
||||
<li><strong>{{ key }}:</strong> <code>{{ line|join(', ') }}</code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-tertiary btn-sm dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ macro.icon('road') }} {{ __('label.trace') }}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" style="width: 500px; font-size: 80%;">
|
||||
<ul class="pl-4 pr-3 my-0">
|
||||
{% for key, line in item.location %}
|
||||
<li><strong>{{ key }}:</strong> <code>{{ line|join(', ') }}</code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{{ item.message|replace(item.context|default([])) }}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.context is iterable %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-tertiary btn-sm dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ macro.icon('project-diagram') }} {{ __('label.context') }}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton" style="width: 500px; font-size: 80%;">
|
||||
<ul class="pl-4 pr-3 my-0">
|
||||
{% for key, line in item.context %}
|
||||
<li><strong>{{ key }}:</strong> <code>{{ line|join(', ') }}</code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
{{ item.createdAt|date('Y-m-d H:i:s') }}<br>
|
||||
{% if item.user %}
|
||||
{{ __('label.user') }}: {{ item.user.username }}<small>(№ {{ item.user.id }})</small>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{{ pager(items, template = '@bolt/helpers/_pager_bootstrap.html.twig', class="justify-content-center") }}
|
||||
|
||||
{% endblock %}
|
||||
@@ -5,10 +5,12 @@ declare(strict_types=1);
|
||||
namespace Bolt\Tests\Security;
|
||||
|
||||
use Bolt\Entity\User;
|
||||
use Bolt\Log\LogHandler;
|
||||
use Bolt\Repository\UserRepository;
|
||||
use Bolt\Security\LoginFormAuthenticator;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||
@@ -31,7 +33,7 @@ class LoginFormAuthenticatorTest extends TestCase
|
||||
->with('bolt_login')
|
||||
->willReturn('test_route');
|
||||
|
||||
$res = $this->getTestObj(null, $router, null, null)->start($this->createMock(Request::class));
|
||||
$res = $this->getTestObj(null, $router, null, null, null)->start($this->createMock(Request::class));
|
||||
$this->assertSame('test_route', $res->getTargetUrl());
|
||||
}
|
||||
|
||||
@@ -44,7 +46,7 @@ class LoginFormAuthenticatorTest extends TestCase
|
||||
'isTokenValid' => true,
|
||||
]);
|
||||
|
||||
$res = $this->getTestObj($userRepository, null, $csrfTokenManager, null)->getUser(self::TEST_TOKEN, $this->createMock(UserProviderInterface::class));
|
||||
$res = $this->getTestObj($userRepository, null, $csrfTokenManager, null, null)->getUser(self::TEST_TOKEN, $this->createMock(UserProviderInterface::class));
|
||||
$this->assertInstanceOf(User::class, $res);
|
||||
}
|
||||
|
||||
@@ -55,17 +57,18 @@ class LoginFormAuthenticatorTest extends TestCase
|
||||
]);
|
||||
|
||||
$this->expectException(InvalidCsrfTokenException::class);
|
||||
$this->getTestObj(null, null, $csrfTokenManager, null)->getUser(self::TEST_TOKEN, $this->createMock(UserProviderInterface::class));
|
||||
$this->getTestObj(null, null, $csrfTokenManager, null, null)->getUser(self::TEST_TOKEN, $this->createMock(UserProviderInterface::class));
|
||||
}
|
||||
|
||||
private function getTestObj(?UserRepository $userRepository, ?RouterInterface $router, ?CsrfTokenManagerInterface $csrfTokenManager, ?UserPasswordEncoderInterface $userPasswordEncoder): LoginFormAuthenticator
|
||||
private function getTestObj(?UserRepository $userRepository, ?RouterInterface $router, ?CsrfTokenManagerInterface $csrfTokenManager, ?UserPasswordEncoderInterface $userPasswordEncoder, ?LoggerInterface $logger): LoginFormAuthenticator
|
||||
{
|
||||
return new LoginFormAuthenticator(
|
||||
$userRepository ?? $this->createMock(UserRepository::class),
|
||||
$router ?? $this->createMock(RouterInterface::class),
|
||||
$csrfTokenManager ?? $this->createMock(CsrfTokenManagerInterface::class),
|
||||
$userPasswordEncoder ?? $this->createMock(UserPasswordEncoderInterface::class),
|
||||
$em ?? $this->createMock(ObjectManager::class)
|
||||
$em ?? $this->createMock(ObjectManager::class),
|
||||
$logger ?? $this->createMock(LoggerInterface::class)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2103,5 +2103,59 @@
|
||||
<target>Download</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="2MtT_h0" name="caption.logviewer">
|
||||
<segment>
|
||||
<source>caption.logviewer</source>
|
||||
<target>Log Viewer</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="FlGE3qB" name="label.request">
|
||||
<segment>
|
||||
<source>label.request</source>
|
||||
<target>Request</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="l71Tprl" name="label.trace">
|
||||
<segment>
|
||||
<source>label.trace</source>
|
||||
<target>Trace</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="29ZEo1r" name="label.context">
|
||||
<segment>
|
||||
<source>label.context</source>
|
||||
<target>Context</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="txRonia" name="label.id">
|
||||
<segment>
|
||||
<source>label.id</source>
|
||||
<target>ID</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="IwLLxxx" name="label.level">
|
||||
<segment>
|
||||
<source>label.level</source>
|
||||
<target>Level</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="51OQi14" name="label.message">
|
||||
<segment>
|
||||
<source>label.message</source>
|
||||
<target>Message</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="S0f6iTL" name="label.timestamp">
|
||||
<segment>
|
||||
<source>label.timestamp</source>
|
||||
<target>Timestamp</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="O2X4xZH" name="label.user">
|
||||
<segment>
|
||||
<source>label.user</source>
|
||||
<target>User</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
Reference in New Issue
Block a user