diff --git a/migrations/Version20241003162915.php b/migrations/Version20241003162915.php new file mode 100644 index 0000000..e0c4f5d --- /dev/null +++ b/migrations/Version20241003162915.php @@ -0,0 +1,37 @@ +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'); + } +} diff --git a/src/Controller/Admin/DashboardController.php b/src/Controller/Admin/DashboardController.php index 811055c..c6c8eb1 100644 --- a/src/Controller/Admin/DashboardController.php +++ b/src/Controller/Admin/DashboardController.php @@ -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; @@ -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); } } diff --git a/src/Controller/Admin/LoginEventCrudController.php b/src/Controller/Admin/LoginEventCrudController.php new file mode 100644 index 0000000..2d84e3e --- /dev/null +++ b/src/Controller/Admin/LoginEventCrudController.php @@ -0,0 +1,44 @@ +add('account'); + } + + public function configureActions(Actions $actions): Actions + { + return $actions + ->disable(Action::DELETE, Action::EDIT, Action::NEW) + ->add(Crud::PAGE_INDEX, Action::DETAIL); + } +} diff --git a/src/Entity/LoginEvent.php b/src/Entity/LoginEvent.php new file mode 100644 index 0000000..a006274 --- /dev/null +++ b/src/Entity/LoginEvent.php @@ -0,0 +1,28 @@ +account; + } + + public function setAccount(?User $account): static + { + $this->account = $account; + + return $this; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index 0447e39..8d0f586 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -76,6 +76,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\OneToMany(targetEntity: HintOpenEvent::class, mappedBy: 'opener', orphanRemoval: true)] private Collection $hintOpenEvents; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: LoginEvent::class, mappedBy: 'account', orphanRemoval: true)] + private Collection $loginEvents; + public function __construct() { $this->solutionEvents = new ArrayCollection(); @@ -83,6 +89,7 @@ public function __construct() $this->comments = new ArrayCollection(); $this->commentLikeEvents = new ArrayCollection(); $this->hintOpenEvents = new ArrayCollection(); + $this->loginEvents = new ArrayCollection(); } public function getId(): ?int @@ -343,4 +350,34 @@ public function removeHintOpenEvent(HintOpenEvent $hintOpenEvent): static return $this; } + + /** + * @return Collection + */ + 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; + } } diff --git a/src/EventSubscriber/LoginSubscriber.php b/src/EventSubscriber/LoginSubscriber.php new file mode 100644 index 0000000..d3d3040 --- /dev/null +++ b/src/EventSubscriber/LoginSubscriber.php @@ -0,0 +1,38 @@ +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', + ]; + } +} diff --git a/src/Repository/LoginEventRepository.php b/src/Repository/LoginEventRepository.php new file mode 100644 index 0000000..a95feb0 --- /dev/null +++ b/src/Repository/LoginEventRepository.php @@ -0,0 +1,35 @@ + + */ +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; + } +} diff --git a/templates/profile/index.html.twig b/templates/profile/index.html.twig index 60841a7..ba73f51 100644 --- a/templates/profile/index.html.twig +++ b/templates/profile/index.html.twig @@ -40,6 +40,10 @@ 觀看影片次數:{{ user.solutionVideoEvents.count }} +
  • + + 登入次數:{{ user.loginEvents.count }} +
  • diff --git a/translations/messages.zh_TW.yaml b/translations/messages.zh_TW.yaml index 4641c2d..56fd815 100644 --- a/translations/messages.zh_TW.yaml +++ b/translations/messages.zh_TW.yaml @@ -44,6 +44,8 @@ Back to App: 返回 App Reindex: 進行搜尋索引 HintOpenEvent: 提示打開事件 Response: 回應 +LoginEvent: 登入事件 +Account: 帳號 challenge.error-type.user: 輸入錯誤 challenge.error-type.server: 伺服器錯誤