Skip to content

Commit

Permalink
[Feature] Admin Signup Notifications (#1242)
Browse files Browse the repository at this point in the history
  • Loading branch information
BentiGorlich committed Dec 10, 2024
1 parent 71c9c0f commit 6bae56c
Show file tree
Hide file tree
Showing 15 changed files with 212 additions and 5 deletions.
1 change: 1 addition & 0 deletions config/packages/doctrine.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ doctrine:
mapping_types:
user_type: string
citext: citext
enumApplicationStatus: string

# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
Expand Down
32 changes: 32 additions & 0 deletions migrations/Version20241124155724.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

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

final class Version20241124155724 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add new_user_id to notification table and notify_on_user_signup to "user" table for the `NewSignupNotification`';
}

public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE notification ADD new_user_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CA7C2D807B FOREIGN KEY (new_user_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_BF5476CA7C2D807B ON notification (new_user_id)');
$this->addSql('ALTER TABLE "user" ADD notify_on_user_signup BOOLEAN DEFAULT TRUE');
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE notification DROP CONSTRAINT FK_BF5476CA7C2D807B');
$this->addSql('DROP INDEX IDX_BF5476CA7C2D807B');
$this->addSql('ALTER TABLE notification DROP new_user_id');
$this->addSql('ALTER TABLE "user" DROP notify_on_user_signup');
}
}
4 changes: 3 additions & 1 deletion src/DTO/UserSettingsDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public function __construct(
#[OA\Property(type: 'array', items: new OA\Items(type: 'string'))]
public ?array $preferredLanguages = null,
public ?string $customCss = null,
public ?bool $ignoreMagazinesCustomCss = null
public ?bool $ignoreMagazinesCustomCss = null,
public ?bool $notifyOnUserSignup = null,
) {
}

Expand All @@ -52,6 +53,7 @@ public function jsonSerialize(): mixed
'preferredLanguages' => $this->preferredLanguages,
'customCss' => $this->customCss,
'ignoreMagazinesCustomCss' => $this->ignoreMagazinesCustomCss,
'notifyOnUserSignup' => $this->notifyOnUserSignup,
];
}

Expand Down
41 changes: 41 additions & 0 deletions src/Entity/NewSignupNotification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace App\Entity;

use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

#[Entity]
class NewSignupNotification extends Notification
{
#[ManyToOne(targetEntity: User::class, cascade: ['remove'])]
#[JoinColumn(nullable: true, onDelete: 'CASCADE')]
public ?User $newUser;

public function getType(): string
{
return 'new_signup';
}

public function getSubject(): ?User
{
return $this->newUser;
}

public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
{
$message = str_replace('%u%', $this->newUser->username, $trans->trans('notification_body_new_signup', locale: $locale));
$title = $trans->trans('notification_title_new_signup', locale: $locale);
$url = $urlGenerator->generate('user_overview', ['username' => $this->newUser->username]);
$slash = $this->newUser->avatar && !str_starts_with('/', $this->newUser->avatar->filePath) ? '/' : '';
$avatarUrl = $this->newUser->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->newUser->avatar->filePath : null;

return new PushNotification($message, $title, actionUrl: $url, avatarUrl: $avatarUrl);
}
}
1 change: 1 addition & 0 deletions src/Entity/Notification.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
'report_created' => 'ReportCreatedNotification',
'report_approved' => 'ReportApprovedNotification',
'report_rejected' => 'ReportRejectedNotification',
'new_signup' => 'NewSignupNotification',
])]
abstract class Notification
{
Expand Down
2 changes: 2 additions & 0 deletions src/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Visibil
public bool $notifyOnNewPostReply = true;
#[Column(type: 'boolean', nullable: false)]
public bool $notifyOnNewPostCommentReply = true;
#[Column(type: 'boolean', nullable: false, options: ['default' => true])]
public bool $notifyOnUserSignup = true;
#[Column(type: 'boolean', nullable: false, options: ['default' => false])]
public bool $addMentionsEntries = false;
#[Column(type: 'boolean', nullable: false, options: ['default' => true])]
Expand Down
13 changes: 11 additions & 2 deletions src/Form/UserSettingsType.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\DTO\UserSettingsDto;
use App\Entity\User;
use App\Form\DataTransformer\FeaturedMagazinesBarTransformer;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
Expand All @@ -19,8 +20,10 @@

class UserSettingsType extends AbstractType
{
public function __construct(private readonly TranslatorInterface $translator)
{
public function __construct(
private readonly TranslatorInterface $translator,
private readonly Security $security,
) {
}

public function buildForm(FormBuilderInterface $builder, array $options): void
Expand Down Expand Up @@ -109,6 +112,12 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
)
->add('submit', SubmitType::class);

/** @var User $user */
$user = $this->security->getUser();
if ($user->isAdmin()) {
$builder->add('notifyOnUserSignup', CheckboxType::class, ['required' => false]);
}

$builder->get('featuredMagazines')->addModelTransformer(
new FeaturedMagazinesBarTransformer()
);
Expand Down
14 changes: 14 additions & 0 deletions src/Message/Notification/SentNewSignupNotificationMessage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace App\Message\Notification;

use App\Message\Contracts\AsyncMessageInterface;

class SentNewSignupNotificationMessage implements AsyncMessageInterface
{
public function __construct(public int $userId)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace App\MessageHandler\Notification;

use App\Message\Contracts\MessageInterface;
use App\Message\Notification\SentNewSignupNotificationMessage;
use App\MessageHandler\MbinMessageHandler;
use App\Repository\UserRepository;
use App\Service\Notification\SignupNotificationManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;

#[AsMessageHandler]
class SentNewSignupNotificationHandler extends MbinMessageHandler
{
public function __construct(
readonly EntityManagerInterface $entityManager,
private readonly UserRepository $userRepository,
private readonly SignupNotificationManager $signupNotificationManager,
) {
parent::__construct($entityManager);
}

public function __invoke(SentNewSignupNotificationMessage $message)
{
$this->workWrapper($message);
}

public function doWork(MessageInterface $message): void
{
if (!($message instanceof SentNewSignupNotificationMessage)) {
throw new \LogicException();
}
$user = $this->userRepository->findOneBy(['id' => $message->userId]);
if (!$user) {
throw new UnrecoverableMessageHandlingException('user not found');
}
$this->signupNotificationManager->sendNewSignupNotification($user);
}
}
37 changes: 37 additions & 0 deletions src/Service/Notification/SignupNotificationManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace App\Service\Notification;

use App\Entity\NewSignupNotification;
use App\Entity\User;
use App\Event\NotificationCreatedEvent;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

readonly class SignupNotificationManager
{
public function __construct(
private EntityManagerInterface $entityManager,
private UserRepository $userRepository,
private EventDispatcherInterface $dispatcher,
) {
}

public function sendNewSignupNotification(User $newUser): void
{
$receivers = $this->userRepository->findAllAdmins();
foreach ($receivers as $receiver) {
if (!$receiver->notifyOnUserSignup) {
continue;
}
$notification = new NewSignupNotification($receiver);
$notification->newUser = $newUser;
$this->entityManager->persist($notification);
$this->dispatcher->dispatch(new NotificationCreatedEvent($notification));
}
$this->entityManager->flush();
}
}
10 changes: 9 additions & 1 deletion src/Service/UserManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use App\Message\ClearDeletedUserMessage;
use App\Message\DeleteImageMessage;
use App\Message\DeleteUserMessage;
use App\Message\Notification\SentNewSignupNotificationMessage;
use App\Message\UserCreatedMessage;
use App\Message\UserUpdatedMessage;
use App\MessageHandler\ClearDeletedUserHandler;
Expand Down Expand Up @@ -173,10 +174,17 @@ public function create(UserDto $dto, bool $verifyUserEmail = true, $rateLimit =
$this->entityManager->persist($user);
$this->entityManager->flush();

if (!$dto->apId) {
try {
$this->bus->dispatch(new SentNewSignupNotificationMessage($user->getId()));
} catch (\Throwable $e) {
}
}

if ($verifyUserEmail) {
try {
$this->bus->dispatch(new UserCreatedMessage($user->getId()));
} catch (\Exception $e) {
} catch (\Throwable $e) {
}
}

Expand Down
7 changes: 6 additions & 1 deletion src/Service/UserSettingsManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public function createDto(User $user): UserSettingsDto
$user->featuredMagazines,
$user->preferredLanguages,
$user->customCss,
$user->ignoreMagazinesCustomCss
$user->ignoreMagazinesCustomCss,
$user->notifyOnUserSignup,
);
}

Expand All @@ -55,6 +56,10 @@ public function update(User $user, UserSettingsDto $dto): void
$user->customCss = $dto->customCss;
$user->ignoreMagazinesCustomCss = $dto->ignoreMagazinesCustomCss;

if (null !== $dto->notifyOnUserSignup) {
$user->notifyOnUserSignup = $dto->notifyOnUserSignup;
}

$this->entityManager->flush();
}
}
5 changes: 5 additions & 0 deletions templates/notifications/_blocks.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,8 @@
<a href="{{ path('magazine_panel_reports', { 'name': notification.report.magazine.name, 'status': 'approved' }) }}#report-id-{{ notification.report.id }}">{{ 'open_report'|trans }}</a>
{% endif %}
{% endblock report_approved_notification %}

{% block new_signup %}
{{ 'notification_title_new_signup'|trans }}<br />
{{ component('user_inline', { user: notification.newUser } ) }}
{% endblock %}
3 changes: 3 additions & 0 deletions templates/user/settings/general.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
{{ form_row(form.notifyOnNewPostCommentReply, {label: 'notify_on_new_post_comment_reply', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.notifyOnNewEntry, {label: 'notify_on_new_entry', row_attr: {class: 'checkbox'}}) }}
{{ form_row(form.notifyOnNewPost, {label: 'notify_on_new_posts', row_attr: {class: 'checkbox'}}) }}
{% if app.user.admin %}
{{ form_row(form.notifyOnUserSignup, {label: 'notify_on_user_signup', row_attr: {class: 'checkbox'}}) }}
{% endif %}
<div class="row actions">
{{ form_row(form.submit, {label: 'save', attr: {class: 'btn btn__primary'}}) }}
</div>
Expand Down
4 changes: 4 additions & 0 deletions translations/messages.en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ notify_on_new_post_comment_reply: Replies to my comments on any posts
notify_on_new_entry: New threads (links or articles) in any magazine to which I'm
subscribed
notify_on_new_posts: New posts in any magazine to which I'm subscribed
notify_on_user_signup: New signups
save: Save
about: About
old_email: Current email
Expand Down Expand Up @@ -875,6 +876,9 @@ notification_title_message: New direct message
notification_title_new_post: New Post
notification_title_removed_post: A post was removed
notification_title_edited_post: A post was edited
notification_title_new_signup: A new user registered
notification_body_new_signup: The user %u% registered.
notification_body2_new_signup_approval: You need to approve the request before they can log in
show_related_magazines: Show random magazines
show_related_entries: Show random threads
show_related_posts: Show random posts
Expand Down

0 comments on commit 6bae56c

Please sign in to comment.