Skip to content
This repository was archived by the owner on Oct 15, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions migrations/Version20241003162915.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241003162915 extends AbstractMigration
{
public function getDescription(): string
{
return 'Login Event';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE login_event (id UUID NOT NULL, account_id INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_3DDECD339B6B5FBA ON login_event (account_id)');
$this->addSql('COMMENT ON COLUMN login_event.id IS \'(DC2Type:ulid)\'');
$this->addSql('COMMENT ON COLUMN login_event.created_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE login_event ADD CONSTRAINT FK_3DDECD339B6B5FBA FOREIGN KEY (account_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE login_event DROP CONSTRAINT FK_3DDECD339B6B5FBA');
$this->addSql('DROP TABLE login_event');
}
}
2 changes: 2 additions & 0 deletions src/Controller/Admin/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Entity\CommentLikeEvent;
use App\Entity\Group;
use App\Entity\HintOpenEvent;
use App\Entity\LoginEvent;
use App\Entity\Question;
use App\Entity\Schema;
use App\Entity\SolutionEvent;
Expand Down Expand Up @@ -54,5 +55,6 @@ public function configureMenuItems(): iterable
yield MenuItem::linkToCrud('SolutionEvent', 'fa fa-check', SolutionEvent::class);
yield MenuItem::linkToCrud('SolutionVideoEvent', 'fa fa-video', SolutionVideoEvent::class);
yield MenuItem::linkToCrud('HintOpenEvent', 'fa fa-lightbulb', HintOpenEvent::class);
yield MenuItem::linkToCrud('LoginEvent', 'fa fa-sign-in', LoginEvent::class);
}
}
44 changes: 44 additions & 0 deletions src/Controller/Admin/LoginEventCrudController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace App\Controller\Admin;

use App\Entity\LoginEvent;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Config\Filters;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;

class LoginEventCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return LoginEvent::class;
}

public function configureFields(string $pageName): iterable
{
return [
IdField::new('id', 'ID'),
AssociationField::new('account', 'Account'),
DateTimeField::new('createdAt', 'Created at'),
];
}

public function configureFilters(Filters $filters): Filters
{
return $filters->add('account');
}

public function configureActions(Actions $actions): Actions
{
return $actions
->disable(Action::DELETE, Action::EDIT, Action::NEW)
->add(Crud::PAGE_INDEX, Action::DETAIL);
}
}
28 changes: 28 additions & 0 deletions src/Entity/LoginEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace App\Entity;

use App\Repository\LoginEventRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: LoginEventRepository::class)]
class LoginEvent extends BaseEvent
{
#[ORM\ManyToOne(inversedBy: 'loginEvents')]
#[ORM\JoinColumn(nullable: false)]
private ?User $account = null;

public function getAccount(): ?User
{
return $this->account;
}

public function setAccount(?User $account): static
{
$this->account = $account;

return $this;
}
}
37 changes: 37 additions & 0 deletions src/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,20 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\OneToMany(targetEntity: HintOpenEvent::class, mappedBy: 'opener', orphanRemoval: true)]
private Collection $hintOpenEvents;

/**
* @var Collection<int, LoginEvent>
*/
#[ORM\OneToMany(targetEntity: LoginEvent::class, mappedBy: 'account', orphanRemoval: true)]
private Collection $loginEvents;

public function __construct()
{
$this->solutionEvents = new ArrayCollection();
$this->solutionVideoEvents = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->commentLikeEvents = new ArrayCollection();
$this->hintOpenEvents = new ArrayCollection();
$this->loginEvents = new ArrayCollection();
}

public function getId(): ?int
Expand Down Expand Up @@ -343,4 +350,34 @@ public function removeHintOpenEvent(HintOpenEvent $hintOpenEvent): static

return $this;
}

/**
* @return Collection<int, LoginEvent>
*/
public function getLoginEvents(): Collection
{
return $this->loginEvents;
}

public function addLoginEvent(LoginEvent $loginEvent): static
{
if (!$this->loginEvents->contains($loginEvent)) {
$this->loginEvents->add($loginEvent);
$loginEvent->setAccount($this);
}

return $this;
}

public function removeLoginEvent(LoginEvent $loginEvent): static
{
if ($this->loginEvents->removeElement($loginEvent)) {
// set the owning side to null (unless already changed)
if ($loginEvent->getAccount() === $this) {
$loginEvent->setAccount(null);
}
}

return $this;
}
}
38 changes: 38 additions & 0 deletions src/EventSubscriber/LoginSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace App\EventSubscriber;

use App\Entity\LoginEvent;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;

class LoginSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
) {
}

public function onSecurityAuthenticationSuccess(AuthenticationSuccessEvent $event): void
{
$user = $event->getAuthenticationToken()->getUser();
\assert($user instanceof User);

$loginEvent = (new LoginEvent())
->setAccount($user);

$this->entityManager->persist($loginEvent);
$this->entityManager->flush();
}

public static function getSubscribedEvents(): array
{
return [
'security.authentication.success' => 'onSecurityAuthenticationSuccess',
];
}
}
35 changes: 35 additions & 0 deletions src/Repository/LoginEventRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace App\Repository;

use App\Entity\LoginEvent;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
* @extends ServiceEntityRepository<LoginEvent>
*/
class LoginEventRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, LoginEvent::class);
}

public function getLoginCount(User $user): int
{
$loginCount = $this->createQueryBuilder('l')
->select('COUNT(l.id)')
->andWhere('l.account = :user')
->setParameter('user', $user)
->getQuery()
->getSingleScalarResult();

\assert(\is_int($loginCount));

return $loginCount;
}
}
4 changes: 4 additions & 0 deletions templates/profile/index.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
<i class="bi bi-eye-fill"></i>
觀看影片次數:{{ user.solutionVideoEvents.count }}
</li>
<li class="list-group-item">
<i class="bi bi-box-arrow-in-right"></i>
登入次數:{{ user.loginEvents.count }}
</li>
</ul>
</turbo-frame>

Expand Down
2 changes: 2 additions & 0 deletions translations/messages.zh_TW.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Back to App: 返回 App
Reindex: 進行搜尋索引
HintOpenEvent: 提示打開事件
Response: 回應
LoginEvent: 登入事件
Account: 帳號

challenge.error-type.user: 輸入錯誤
challenge.error-type.server: 伺服器錯誤
Expand Down