Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow admins to view user IP addresses in the admin panel users table and users page #1009

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ KBIN_HEADER_LOGO=false
KBIN_FEDERATION_PAGE_ENABLED=true
MBIN_DEFAULT_THEME=default

# If you are running Mbin behind a reverse proxy, uncomment the line below and adjust the proxy address/range below
# to your server's IP address if it does not already fall within the private IP spaces specified.
#TRUSTED_PROXIES=::1,127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
TRUSTED_PROXIES=

# Max image filesize (in bytes)
# This should be set to <= `upload_max_filesize` and `post_max_size` in the server's php.ini file
MAX_IMAGE_BYTES=6000000
Expand Down
5 changes: 5 additions & 0 deletions .env.example_docker
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ KBIN_HEADER_LOGO=false
KBIN_FEDERATION_PAGE_ENABLED=true
MBIN_DEFAULT_THEME=default

# If you are running Mbin behind a reverse proxy, uncomment the line below and adjust the proxy address/range below
# to your server's IP address if it does not already fall within the private IP spaces specified.
#TRUSTED_PROXIES=::1,127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
TRUSTED_PROXIES=

# Max image filesize (in bytes)
# This should be set to <= `upload_max_filesize` and `post_max_size` in the server's php.ini file
MAX_IMAGE_BYTES=6000000
Expand Down
2 changes: 2 additions & 0 deletions config/packages/framework.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ framework:
annotations: false #no longer supported
http_method_override: false
handle_all_throwables: true
trusted_proxies: '%env(string:default::TRUSTED_PROXIES)%'
trusted_headers: ['x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix']

# Enables session support. Note that the session will ONLY be started if you read or write from it.
# Remove or comment this section to explicitly disable session support.
Expand Down
29 changes: 29 additions & 0 deletions migrations/Version20240808230919.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?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 Version20240808230919 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add user IP column';
}

public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE "user" ADD ip VARCHAR(255) DEFAULT NULL');
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE "user" DROP ip');
}
}
3 changes: 3 additions & 0 deletions src/Controller/Entry/Comment/EntryCommentEditController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,6 +28,7 @@ class EntryCommentEditController extends AbstractController
public function __construct(
private readonly EntryCommentManager $manager,
private readonly EntryCommentRepository $repository,
private readonly IpResolver $ipResolver
) {
}

Expand All @@ -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);
Expand Down
8 changes: 6 additions & 2 deletions src/Controller/Post/PostEditController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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')]
Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -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])]
Expand Down
3 changes: 3 additions & 0 deletions src/Security/GithubAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
) {
Expand Down Expand Up @@ -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());
Expand Down
9 changes: 8 additions & 1 deletion src/Security/UserChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace App\Security;

use App\Entity\User as AppUser;
use App\Service\IpResolver;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
Expand All @@ -16,7 +18,9 @@ class UserChecker implements UserCheckerInterface
{
public function __construct(
private readonly TranslatorInterface $translator,
private readonly UrlGeneratorInterface $urlGenerator
private readonly UrlGeneratorInterface $urlGenerator,
private readonly EntityManagerInterface $entityManager,
private readonly IpResolver $ipResolver
) {
}

Expand Down Expand Up @@ -45,5 +49,8 @@ public function checkPostAuth(UserInterface $user): void
if (!$user instanceof AppUser) {
return;
}

$user->ip = $this->ipResolver->resolve();
$this->entityManager->flush();
}
}
8 changes: 8 additions & 0 deletions src/Service/EntryCommentManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/Service/EntryManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/Service/PostCommentManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/Service/PostManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 7 additions & 1 deletion templates/admin/users.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@
<thead>
<tr>
<th>{{ 'username'|trans }}</th>
{% if withFederated is not defined or withFederated is same as false %}
<th>{{ 'email'|trans }}</th>
<th>{{ 'IP' }}</th>
This conversation was marked as resolved.
Show resolved Hide resolved
{% endif %}
<th>{{ 'created_at'|trans }}</th>
<th>{{ 'last_active'|trans }}</th>
</tr>
Expand All @@ -79,7 +82,10 @@
{% for user in users %}
<tr>
<td>{{ component('user_inline', {user: user}) }}</td>
<td>{{ user.apId ? '-' : user.email }}</td>
{% if withFederated is not defined or withFederated is same as false %}
<td>{{ user.email }}</td>
<td>{{ user.ip }}</td>
This conversation was marked as resolved.
Show resolved Hide resolved
{% endif %}
<td>{{ component('date', {date: user.createdAt}) }}</td>
<td>{{ component('date', {date: user.lastActive}) }}</td>
</tr>
Expand Down
5 changes: 5 additions & 0 deletions templates/user/_info.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
{{ component('user_actions', {user: user}) }}
{% endif %}
<ul class="info">
{% if app.user is defined and app.user is not null and app.user.admin() and user.apId is null %}
<li>{{ 'IP' }}: <span>{{ user.ip }}</span></li>
<li>{{ 'email'|trans }}: <span>{{ user.email }}</span></li>
{% endif %}

<li>{{ 'joined'|trans }}: {{ component('date', {date: user.createdAt}) }}</li>
<li>{{ 'cake_day'|trans }}: <div><i class="fa-solid fa-cake" aria-hidden="true"></i> <span>{{ user.createdAt|format_date('short', '', null, 'gregorian', mbin_lang()) }}</span></div></li>
{% if app.user is defined and app.user is not null and app.user.admin() and user.apId is not null %}
Expand Down
Loading