diff --git a/migrations/Version20240808230919.php b/migrations/Version20240808230919.php new file mode 100644 index 000000000..c360f952a --- /dev/null +++ b/migrations/Version20240808230919.php @@ -0,0 +1,29 @@ +addSql('ALTER TABLE "user" ADD ip VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE "user" DROP ip'); + } +} diff --git a/src/Controller/Entry/Comment/EntryCommentEditController.php b/src/Controller/Entry/Comment/EntryCommentEditController.php index 4d9c03763..82ecf17bf 100644 --- a/src/Controller/Entry/Comment/EntryCommentEditController.php +++ b/src/Controller/Entry/Comment/EntryCommentEditController.php @@ -13,6 +13,7 @@ use App\PageView\EntryCommentPageView; use App\Repository\EntryCommentRepository; use App\Service\EntryCommentManager; +use App\Service\IpResolver; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; @@ -27,6 +28,7 @@ class EntryCommentEditController extends AbstractController public function __construct( private readonly EntryCommentManager $manager, private readonly EntryCommentRepository $repository, + private readonly IpResolver $ipResolver ) { } @@ -44,6 +46,7 @@ public function __invoke( $dto = $this->manager->createDto($comment); $form = $this->getForm($dto, $comment); + $dto->ip = $this->ipResolver->resolve(); try { // Could thrown an error on event handlers (eg. onPostSubmit if a user upload an incorrect image) $form->handleRequest($request); diff --git a/src/Controller/Post/PostEditController.php b/src/Controller/Post/PostEditController.php index c4e627e5e..fa0e3a2f7 100644 --- a/src/Controller/Post/PostEditController.php +++ b/src/Controller/Post/PostEditController.php @@ -10,6 +10,7 @@ use App\Form\PostType; use App\PageView\PostCommentPageView; use App\Repository\PostCommentRepository; +use App\Service\IpResolver; use App\Service\PostManager; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\JsonResponse; @@ -20,8 +21,10 @@ class PostEditController extends AbstractController { - public function __construct(private readonly PostManager $manager) - { + public function __construct( + private readonly PostManager $manager, + private readonly IpResolver $ipResolver + ) { } #[IsGranted('ROLE_USER')] @@ -37,6 +40,7 @@ public function __invoke( $dto = $this->manager->createDto($post); $form = $this->createForm(PostType::class, $dto); + $dto->ip = $this->ipResolver->resolve(); try { // Could thrown an error on event handlers (eg. onPostSubmit if a user upload an incorrect image) $form->handleRequest($request); diff --git a/src/Entity/User.php b/src/Entity/User.php index 8c39eca36..a03912fcf 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -93,6 +93,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Visibil public ?Image $cover = null; #[Column(type: 'string', unique: true, nullable: false)] public string $email; + #[Column(type: 'string', nullable: true)] + public ?string $ip = null; #[Column(type: 'string', unique: true, nullable: false)] public string $username; #[Column(type: 'json', nullable: false, options: ['jsonb' => true])] diff --git a/src/Security/GithubAuthenticator.php b/src/Security/GithubAuthenticator.php index 476fd6c18..97f5e60c0 100644 --- a/src/Security/GithubAuthenticator.php +++ b/src/Security/GithubAuthenticator.php @@ -6,6 +6,7 @@ use App\DTO\UserDto; use App\Entity\User; +use App\Service\IpResolver; use App\Service\SettingsManager; use App\Service\UserManager; use App\Utils\Slugger; @@ -31,6 +32,7 @@ public function __construct( private readonly RouterInterface $router, private readonly EntityManagerInterface $entityManager, private readonly UserManager $userManager, + private readonly IpResolver $ipResolver, private readonly Slugger $slugger, private readonly SettingsManager $settingsManager ) { @@ -84,6 +86,7 @@ public function authenticate(Request $request): Passport ); $dto->plainPassword = bin2hex(random_bytes(20)); + $dto->ip = $this->ipResolver->resolve(); $user = $this->userManager->create($dto, false); $user->oauthGithubId = \strval($githubUser->getId()); diff --git a/src/Security/UserChecker.php b/src/Security/UserChecker.php index 945bfae53..806249995 100644 --- a/src/Security/UserChecker.php +++ b/src/Security/UserChecker.php @@ -5,7 +5,9 @@ namespace App\Security; use App\Entity\User as AppUser; +use App\Service\IpResolver; use App\Service\UserManager; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException; @@ -18,6 +20,8 @@ class UserChecker implements UserCheckerInterface public function __construct( private readonly TranslatorInterface $translator, private readonly UrlGeneratorInterface $urlGenerator, + private readonly EntityManagerInterface $entityManager, + private readonly IpResolver $ipResolver, private readonly UserManager $userManager ) { } @@ -55,5 +59,8 @@ public function checkPostAuth(UserInterface $user): void if (!$user instanceof AppUser) { return; } + + $user->ip = $this->ipResolver->resolve(); + $this->entityManager->flush(); } } diff --git a/src/Service/EntryCommentManager.php b/src/Service/EntryCommentManager.php index adaa6a4ed..eaa5c9119 100644 --- a/src/Service/EntryCommentManager.php +++ b/src/Service/EntryCommentManager.php @@ -48,6 +48,10 @@ public function __construct( public function create(EntryCommentDto $dto, User $user, $rateLimit = true): EntryComment { + if (!$user->apId) { + $user->ip = $dto->ip; + } + if ($rateLimit) { $limiter = $this->entryCommentLimiter->create($dto->ip); if ($limiter && false === $limiter->consume()->isAccepted()) { @@ -114,6 +118,10 @@ public function canUserEditComment(EntryComment $comment, User $user): bool public function edit(EntryComment $comment, EntryCommentDto $dto, ?User $editedByUser = null): EntryComment { + if (null !== $editedByUser && !$editedByUser->apId) { + $editedByUser->ip = $dto->ip; + } + Assert::same($comment->entry->getId(), $dto->entry->getId()); $comment->body = $dto->body; diff --git a/src/Service/EntryManager.php b/src/Service/EntryManager.php index 0d600a1f9..c3cba3e7a 100644 --- a/src/Service/EntryManager.php +++ b/src/Service/EntryManager.php @@ -75,6 +75,10 @@ public function __construct( */ public function create(EntryDto $dto, User $user, bool $rateLimit = true, bool $stickyIt = false): Entry { + if (!$user->apId) { + $user->ip = $dto->ip; + } + if ($rateLimit) { $limiter = $this->entryLimiter->create($dto->ip); if (false === $limiter->consume()->isAccepted()) { @@ -182,6 +186,10 @@ public function canUserEditEntry(Entry $entry, User $user): bool public function edit(Entry $entry, EntryDto $dto, User $editedBy): Entry { + if (!$editedBy->apId) { + $editedBy->ip = $dto->ip; + } + Assert::same($entry->magazine->getId(), $dto->magazine->getId()); $entry->title = $dto->title; diff --git a/src/Service/PostCommentManager.php b/src/Service/PostCommentManager.php index 2b57336fb..659c86118 100644 --- a/src/Service/PostCommentManager.php +++ b/src/Service/PostCommentManager.php @@ -54,6 +54,10 @@ public function __construct( */ public function create(PostCommentDto $dto, User $user, $rateLimit = true): PostComment { + if (!$user->apId) { + $user->ip = $dto->ip; + } + if ($rateLimit) { $limiter = $this->postCommentLimiter->create($dto->ip); if ($limiter && false === $limiter->consume()->isAccepted()) { @@ -122,6 +126,10 @@ public function canUserEditPostComment(PostComment $postComment, User $user): bo */ public function edit(PostComment $comment, PostCommentDto $dto, ?User $editedBy = null): PostComment { + if (null !== $editedBy && !$editedBy->apId) { + $editedBy->ip = $dto->ip; + } + Assert::same($comment->post->getId(), $dto->post->getId()); $comment->body = $dto->body; diff --git a/src/Service/PostManager.php b/src/Service/PostManager.php index 9e3b7dccf..1c9464ddc 100644 --- a/src/Service/PostManager.php +++ b/src/Service/PostManager.php @@ -66,6 +66,10 @@ public function __construct( */ public function create(PostDto $dto, User $user, $rateLimit = true, bool $stickyIt = false): Post { + if (!$user->apId) { + $user->ip = $dto->ip; + } + if ($rateLimit) { $limiter = $this->postLimiter->create($dto->ip); if ($limiter && false === $limiter->consume()->isAccepted()) { @@ -133,6 +137,10 @@ public function canUserEditPost(Post $post, User $user): bool public function edit(Post $post, PostDto $dto, ?User $editedBy = null): Post { + if (null !== $editedBy && !$editedBy->apId) { + $editedBy->ip = $dto->ip; + } + Assert::same($post->magazine->getId(), $dto->magazine->getId()); $post->body = $dto->body; diff --git a/templates/admin/users.html.twig b/templates/admin/users.html.twig index 9949452bd..8b87daf44 100644 --- a/templates/admin/users.html.twig +++ b/templates/admin/users.html.twig @@ -70,7 +70,10 @@ {{ 'username'|trans }} - {{ 'email'|trans }} + {% if withFederated is not defined or withFederated is same as false %} + {{ 'email'|trans }} + {{ 'IP' }} + {% endif %} {{ 'created_at'|trans }} {{ 'last_active'|trans }} @@ -79,7 +82,10 @@ {% for user in users %} {{ component('user_inline', {user: user}) }} - {{ user.apId ? '-' : user.email }} + {% if withFederated is not defined or withFederated is same as false %} + {{ user.email }} + {{ user.ip }} + {% endif %} {{ component('date', {date: user.createdAt}) }} {{ component('date', {date: user.lastActive}) }} diff --git a/templates/user/_info.html.twig b/templates/user/_info.html.twig index d5d64430c..4afa8037f 100644 --- a/templates/user/_info.html.twig +++ b/templates/user/_info.html.twig @@ -23,6 +23,11 @@ {{ component('user_actions', {user: user}) }} {% endif %}