From 396351124372fac209a24d05e5d117808e42127d Mon Sep 17 00:00:00 2001
From: Ilya Stepenko <38462532+VampireAotD@users.noreply.github.com>
Date: Tue, 27 Aug 2024 16:46:31 +0300
Subject: [PATCH] Refactored code
---
.github/workflows/frontend-tests.yml | 2 +-
.github/workflows/lint.yml | 2 +-
src/.env.example | 1 +
src/.env.testing | 1 +
...elegramUserDTO.php => TelegramUserDTO.php} | 20 +++++-
.../Telegram/TelegramUserException.php | 15 +++++
.../Telegram/TelegramController.php | 29 ++-------
.../Telegram/ValidateSignatureMiddleware.php | 22 +++----
.../Http/Requests/Telegram/AssignRequest.php | 2 +-
.../Jobs/Telegram/RegisterTelegramUserJob.php | 9 ++-
src/app/Models/User.php | 12 ++++
src/app/Services/TelegramUserService.php | 58 +++++++++++++-----
src/app/Telegram/Commands/StartCommand.php | 6 +-
src/config/mail.php | 9 +++
.../integration-list/IntegrationList.vue | 23 +++----
.../Concerns/Fake/CanCreateFakeUsers.php | 10 +++
.../Telegram/TelegramControllerTest.php | 40 +++---------
.../Services/TelegramUserServiceTest.php | 61 +++++++++++++++++++
18 files changed, 214 insertions(+), 108 deletions(-)
rename src/app/DTO/Service/Telegram/User/{RegisterTelegramUserDTO.php => TelegramUserDTO.php} (54%)
create mode 100644 src/app/Exceptions/Service/Telegram/TelegramUserException.php
create mode 100644 src/tests/Feature/Services/TelegramUserServiceTest.php
diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml
index 62d959f9..771d9ba2 100644
--- a/.github/workflows/frontend-tests.yml
+++ b/.github/workflows/frontend-tests.yml
@@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
node-version: [ 20.x ]
- pnpm-version: [ 9.3 ]
+ pnpm-version: [ 9.7 ]
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 6e25594b..c2f82016 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
node-version: [ 20.x ]
- pnpm-version: [ 9.3 ]
+ pnpm-version: [ 9.7 ]
steps:
- uses: actions/checkout@v4
diff --git a/src/.env.example b/src/.env.example
index 5c7aa159..b7ae80e8 100644
--- a/src/.env.example
+++ b/src/.env.example
@@ -59,6 +59,7 @@ MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=example@gmail.com
MAIL_FROM_NAME="${APP_NAME}"
+MAIL_TEMPORARY_DOMAIN=anilibrary-temporary.mail
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
diff --git a/src/.env.testing b/src/.env.testing
index 765410f0..50763f0a 100644
--- a/src/.env.testing
+++ b/src/.env.testing
@@ -59,6 +59,7 @@ MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=testing@gmail.com
MAIL_FROM_NAME="${APP_NAME}"
+MAIL_TEMPORARY_DOMAIN=anilibrary-testing-temporary.mail
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
diff --git a/src/app/DTO/Service/Telegram/User/RegisterTelegramUserDTO.php b/src/app/DTO/Service/Telegram/User/TelegramUserDTO.php
similarity index 54%
rename from src/app/DTO/Service/Telegram/User/RegisterTelegramUserDTO.php
rename to src/app/DTO/Service/Telegram/User/TelegramUserDTO.php
index 04faa589..946b47c8 100644
--- a/src/app/DTO/Service/Telegram/User/RegisterTelegramUserDTO.php
+++ b/src/app/DTO/Service/Telegram/User/TelegramUserDTO.php
@@ -4,21 +4,35 @@
namespace App\DTO\Service\Telegram\User;
+use App\DTO\Contracts\FromArray;
use Illuminate\Contracts\Support\Arrayable;
/**
* @template-implements Arrayable
*/
-final readonly class RegisterTelegramUserDTO implements Arrayable
+final readonly class TelegramUserDTO implements FromArray, Arrayable
{
public function __construct(
public int $telegramId,
public ?string $firstName = null,
public ?string $lastName = null,
- public ?string $userName = null
+ public ?string $username = null
) {
}
+ /**
+ * @param array{id: int, first_name?: string, last_name?: string, username?: string} $data
+ */
+ public static function fromArray(array $data): self
+ {
+ return new self(
+ $data['id'],
+ $data['first_name'] ?? null,
+ $data['last_name'] ?? null,
+ $data['username'] ?? null
+ );
+ }
+
/**
* Get the instance as an array.
*
@@ -30,7 +44,7 @@ public function toArray(): array
'telegram_id' => $this->telegramId,
'first_name' => $this->firstName,
'last_name' => $this->lastName,
- 'username' => $this->userName,
+ 'username' => $this->username,
];
}
}
diff --git a/src/app/Exceptions/Service/Telegram/TelegramUserException.php b/src/app/Exceptions/Service/Telegram/TelegramUserException.php
new file mode 100644
index 00000000..15cb37db
--- /dev/null
+++ b/src/app/Exceptions/Service/Telegram/TelegramUserException.php
@@ -0,0 +1,15 @@
+get('id'),
- $request->get('first_name'),
- $request->get('last_name'),
- $request->get('username'),
- );
-
try {
- $this->telegramUserService->createAndAttach($request->user(), $dto);
+ $this->telegramUserService->assign($request->user(), TelegramUserDTO::fromArray($request->validated()));
+
+ return back();
} catch (Throwable $e) {
Log::error('Failed to assign telegram user', [
'exception_trace' => $e->getTraceAsString(),
@@ -41,20 +35,5 @@ public function assign(AssignRequest $request): RedirectResponse
return back()->withErrors(['message' => $e->getMessage()]);
}
-
- return back();
- }
-
- /**
- * @param Request $request
- * @return RedirectResponse
- */
- public function detach(Request $request): RedirectResponse
- {
- if (!$request->user()?->telegramUser()?->delete()) {
- return back()->withErrors(['message' => 'Could not revoke Telegram account']);
- }
-
- return back();
}
}
diff --git a/src/app/Http/Middleware/Telegram/ValidateSignatureMiddleware.php b/src/app/Http/Middleware/Telegram/ValidateSignatureMiddleware.php
index 13554a10..bab858c9 100644
--- a/src/app/Http/Middleware/Telegram/ValidateSignatureMiddleware.php
+++ b/src/app/Http/Middleware/Telegram/ValidateSignatureMiddleware.php
@@ -4,12 +4,17 @@
namespace App\Http\Middleware\Telegram;
+use App\Services\TelegramUserService;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
-class ValidateSignatureMiddleware
+readonly class ValidateSignatureMiddleware
{
+ public function __construct(private TelegramUserService $telegramUserService)
+ {
+ }
+
/**
* Handle an incoming request.
*
@@ -22,19 +27,8 @@ public function handle(Request $request, Closure $next): Response
$telegramHash = $request->get('hash');
abort_if(!$telegramHash, Response::HTTP_BAD_REQUEST, 'Missing Telegram signature');
- $hashedToken = hash('sha256', config('nutgram.token'), true);
-
- // Transform all request fields into signature
- $signature = $request->collect()
- ->except('hash')
- ->map(fn(mixed $value, string $key) => sprintf('%s=%s', $key, $value))
- ->values()
- ->sort()
- ->implode(PHP_EOL);
-
- $hashedSignature = hash_hmac('sha256', $signature, $hashedToken);
-
- abort_if(!hash_equals($telegramHash, $hashedSignature), Response::HTTP_FORBIDDEN);
+ $signature = $this->telegramUserService->generateSignature($request->toArray());
+ abort_if(!hash_equals($telegramHash, $signature), Response::HTTP_FORBIDDEN);
return $next($request);
}
diff --git a/src/app/Http/Requests/Telegram/AssignRequest.php b/src/app/Http/Requests/Telegram/AssignRequest.php
index a273511c..3b4b24fb 100644
--- a/src/app/Http/Requests/Telegram/AssignRequest.php
+++ b/src/app/Http/Requests/Telegram/AssignRequest.php
@@ -25,7 +25,7 @@ public function authorize(): bool
public function rules(): array
{
return [
- 'id' => 'required|int',
+ 'id' => 'required|int|unique:telegram_users,telegram_id',
'auth_date' => 'required|int',
'hash' => 'required|string',
'first_name' => 'nullable|string',
diff --git a/src/app/Jobs/Telegram/RegisterTelegramUserJob.php b/src/app/Jobs/Telegram/RegisterTelegramUserJob.php
index 672b931c..dd97f04e 100644
--- a/src/app/Jobs/Telegram/RegisterTelegramUserJob.php
+++ b/src/app/Jobs/Telegram/RegisterTelegramUserJob.php
@@ -4,14 +4,16 @@
namespace App\Jobs\Telegram;
-use App\DTO\Service\Telegram\User\RegisterTelegramUserDTO;
+use App\DTO\Service\Telegram\User\TelegramUserDTO;
use App\Enums\QueueEnum;
+use App\Exceptions\Service\Telegram\TelegramUserException;
use App\Services\TelegramUserService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
+use Throwable;
class RegisterTelegramUserJob implements ShouldQueue
{
@@ -23,16 +25,17 @@ class RegisterTelegramUserJob implements ShouldQueue
/**
* Create a new job instance.
*/
- public function __construct(public readonly RegisterTelegramUserDTO $dto)
+ public function __construct(public readonly TelegramUserDTO $dto)
{
$this->onQueue(QueueEnum::TELEGRAM_QUEUE->value)->onConnection('redis');
}
/**
* Execute the job.
+ * @throws TelegramUserException|Throwable
*/
public function handle(TelegramUserService $telegramUserService): void
{
- $telegramUserService->upsert($this->dto);
+ $telegramUserService->register($this->dto);
}
}
diff --git a/src/app/Models/User.php b/src/app/Models/User.php
index 0f8f1c2b..476032cb 100644
--- a/src/app/Models/User.php
+++ b/src/app/Models/User.php
@@ -6,6 +6,7 @@
use App\Notifications\Auth\VerifyEmailNotification;
use Illuminate\Contracts\Auth\MustVerifyEmail;
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasOne;
@@ -56,4 +57,15 @@ public function sendEmailVerificationNotification(): void
{
$this->notify(new VerifyEmailNotification());
}
+
+ /**
+ * @psalm-suppress TooManyTemplateParams Suppressed because PHPStan needs description, but Psalm conflicts with it
+ * @return Attribute
+ */
+ protected function hasTemporaryEmail(): Attribute
+ {
+ return Attribute::make(
+ get: fn(): bool => str_ends_with($this->email, config('mail.temporary_domain')),
+ )->shouldCache();
+ }
}
diff --git a/src/app/Services/TelegramUserService.php b/src/app/Services/TelegramUserService.php
index 78825a1c..e75adcd3 100644
--- a/src/app/Services/TelegramUserService.php
+++ b/src/app/Services/TelegramUserService.php
@@ -4,43 +4,69 @@
namespace App\Services;
-use App\DTO\Service\Telegram\User\RegisterTelegramUserDTO;
+use App\DTO\Service\Telegram\User\TelegramUserDTO;
+use App\Exceptions\Service\Telegram\TelegramUserException;
use App\Models\TelegramUser;
use App\Models\User;
use App\Repositories\TelegramUser\TelegramUserRepositoryInterface;
+use App\Repositories\User\UserRepositoryInterface;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Str;
use Throwable;
final readonly class TelegramUserService
{
- public function __construct(private TelegramUserRepositoryInterface $telegramUserRepository)
+ public function __construct(
+ private UserRepositoryInterface $userRepository,
+ private TelegramUserRepositoryInterface $telegramUserRepository
+ ) {
+ }
+
+ public function generateSignature(array $data = []): string
{
+ $token = hash('sha256', config('nutgram.token'), true);
+
+ $signature = collect($data)
+ ->except('hash')
+ ->map(fn(mixed $value, string $key): string => sprintf('%s=%s', $key, $value))
+ ->values()
+ ->sort()
+ ->implode(PHP_EOL);
+
+ return hash_hmac('sha256', $signature, $token);
}
- public function upsert(RegisterTelegramUserDTO $dto): TelegramUser
+ public function upsert(TelegramUserDTO $dto): TelegramUser
{
return $this->telegramUserRepository->upsert($dto->toArray());
}
/**
- * @throws Throwable
+ * @throws TelegramUserException|Throwable
*/
- public function createAndAttach(User $user, RegisterTelegramUserDTO $dto): TelegramUser
+ public function register(TelegramUserDTO $dto): TelegramUser
{
- return DB::transaction(function () use ($dto, $user): TelegramUser {
- /** @var TelegramUser $telegramUser */
- $telegramUser = TelegramUser::withTrashed()->updateOrCreate(
- ['telegram_id' => $dto->telegramId],
- $dto->toArray()
- );
+ if ($this->telegramUserRepository->findByTelegramId($dto->telegramId)) {
+ throw TelegramUserException::userAlreadyRegistered();
+ }
- if ($telegramUser->trashed()) {
- $telegramUser->restore();
- }
+ $domain = config('mail.temporary_domain');
- $telegramUser->user()->associate($user)->save();
+ return DB::transaction(function () use ($dto, $domain): TelegramUser {
+ $user = $this->userRepository->upsert([
+ 'name' => $dto->telegramId,
+ 'email' => "$dto->telegramId@$domain",
+ 'password' => Str::random(),
+ ]);
- return $telegramUser;
+ $user->markEmailAsVerified();
+
+ return $user->telegramUser()->create($dto->toArray());
});
}
+
+ public function assign(User $user, TelegramUserDTO $dto): void
+ {
+ $user->telegramUser()->updateOrCreate(['telegram_id' => $dto->telegramId], $dto->toArray());
+ }
}
diff --git a/src/app/Telegram/Commands/StartCommand.php b/src/app/Telegram/Commands/StartCommand.php
index 808d4f6d..0f8fcc4e 100644
--- a/src/app/Telegram/Commands/StartCommand.php
+++ b/src/app/Telegram/Commands/StartCommand.php
@@ -4,7 +4,7 @@
namespace App\Telegram\Commands;
-use App\DTO\Service\Telegram\User\RegisterTelegramUserDTO;
+use App\DTO\Service\Telegram\User\TelegramUserDTO;
use App\Enums\Telegram\Actions\ActionEnum;
use App\Enums\Telegram\Buttons\CommandButtonEnum;
use App\Jobs\Telegram\RegisterTelegramUserJob;
@@ -25,11 +25,11 @@ public function handle(Nutgram $bot): void
if ($user && !$user->is_bot) {
RegisterTelegramUserJob::dispatch(
- new RegisterTelegramUserDTO(
+ new TelegramUserDTO(
telegramId: $user->id,
firstName : $user->first_name,
lastName : $user->last_name,
- userName : $user->username
+ username : $user->username
)
);
}
diff --git a/src/config/mail.php b/src/config/mail.php
index 788cc501..f2e01f6d 100644
--- a/src/config/mail.php
+++ b/src/config/mail.php
@@ -127,4 +127,13 @@
],
],
+ /*-------------------------------------------------------------------------
+ | Email Domain
+ |--------------------------------------------------------------------------
+ |
+ | Temporary email domain used for registration and verification.
+ |
+ */
+ 'temporary_domain' => env('MAIL_TEMPORARY_DOMAIN'),
+
];
diff --git a/src/resources/js/features/profile/integration-list/IntegrationList.vue b/src/resources/js/features/profile/integration-list/IntegrationList.vue
index 3a4fae4d..e52d1fcb 100644
--- a/src/resources/js/features/profile/integration-list/IntegrationList.vue
+++ b/src/resources/js/features/profile/integration-list/IntegrationList.vue
@@ -3,9 +3,10 @@ import { TelegramLoginWidget } from '@/features/telegram/login-widget';
import { TelegramUser } from '@/entities/telegram-user';
import { router, usePage } from '@inertiajs/vue3';
import { computed } from 'vue';
-import { Button } from '@/shared/ui/button';
+import { useToast } from 'primevue/usetoast';
const page = usePage();
+const toast = useToast();
const telegramUser = computed(() => page.props.auth.user.telegram_user);
const handleTelegramLogin = (user: TelegramUser) => {
@@ -13,12 +14,14 @@ const handleTelegramLogin = (user: TelegramUser) => {
router.post(route('telegram.assign'), payload, {
preserveScroll: true,
- });
-};
-
-const revokeTelegramAccount = () => {
- router.delete(route('telegram.detach'), {
- preserveScroll: true,
+ onError: (response) => {
+ toast.add({
+ summary: response?.id,
+ severity: 'error',
+ life: 2000,
+ closable: true,
+ });
+ },
});
};
@@ -51,14 +54,12 @@ const revokeTelegramAccount = () => {
Connected as
{{ telegramUser.username ?? telegramUser.telegram_id }}
-
-
+
+
diff --git a/src/tests/Concerns/Fake/CanCreateFakeUsers.php b/src/tests/Concerns/Fake/CanCreateFakeUsers.php
index 485e44c0..6ec04806 100644
--- a/src/tests/Concerns/Fake/CanCreateFakeUsers.php
+++ b/src/tests/Concerns/Fake/CanCreateFakeUsers.php
@@ -5,6 +5,7 @@
namespace Tests\Concerns\Fake;
use App\Enums\RoleEnum;
+use App\Models\TelegramUser;
use App\Models\User;
trait CanCreateFakeUsers
@@ -28,4 +29,13 @@ protected function createAdmin(array $data = []): User
{
return $this->createUser($data)->assignRole(RoleEnum::ADMIN);
}
+
+ protected function createUserWithTelegramAccount(): User
+ {
+ $user = $this->createUser();
+
+ $user->telegramUser()->save(TelegramUser::factory()->make());
+
+ return $user;
+ }
}
diff --git a/src/tests/Feature/Http/Controllers/Telegram/TelegramControllerTest.php b/src/tests/Feature/Http/Controllers/Telegram/TelegramControllerTest.php
index 23f6fd11..a93cd39b 100644
--- a/src/tests/Feature/Http/Controllers/Telegram/TelegramControllerTest.php
+++ b/src/tests/Feature/Http/Controllers/Telegram/TelegramControllerTest.php
@@ -5,6 +5,7 @@
namespace Tests\Feature\Http\Controllers\Telegram;
use App\Models\TelegramUser;
+use App\Services\TelegramUserService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\Concerns\Fake\CanCreateFakeUsers;
@@ -16,18 +17,13 @@ class TelegramControllerTest extends TestCase
use WithFaker;
use CanCreateFakeUsers;
- private function createValidSignature(array $data = []): string
- {
- $hashedToken = hash('sha256', config('nutgram.token'), true);
+ private TelegramUserService $telegramUserService;
- $signature = collect($data)
- ->except('hash')
- ->map(fn(mixed $value, string $key) => sprintf('%s=%s', $key, $value))
- ->values()
- ->sort()
- ->implode(PHP_EOL);
+ protected function setUp(): void
+ {
+ parent::setUp();
- return hash_hmac('sha256', $signature, $hashedToken);
+ $this->telegramUserService = $this->app->make(TelegramUserService::class);
}
private function getValidTelegramData(): array
@@ -40,15 +36,15 @@ private function getValidTelegramData(): array
'username' => $this->faker->userName,
];
- return [...$data, 'hash' => $this->createValidSignature($data)];
+ return [...$data, 'hash' => $this->telegramUserService->generateSignature($data)];
}
- public function testUserCannotConnectToTelegramWithoutTelegramSignature(): void
+ public function testUserCannotAssignTelegramAccountWithoutTelegramSignature(): void
{
$this->actingAs($this->createUser())->post(route('telegram.assign'))->assertBadRequest();
}
- public function testUserCannotConnectToTelegramWithInvalidTelegramSignature(): void
+ public function testUserCannotAssignTelegramAccountWithInvalidTelegramSignature(): void
{
$this->actingAs($this->createUser())
->post(route('telegram.assign'), ['hash' => $this->faker->word])
@@ -60,6 +56,7 @@ public function testUserCannotAssignTelegramAccountIfHeAlreadyHasOne(): void
$user = $this->createUser();
$user->telegramUser()->save(TelegramUser::factory()->make());
+ // RedirectIfHasAssignedUserMiddleware is applied to this route, it will handle this case
$this->actingAs($user)
->post(route('telegram.assign'), $this->getValidTelegramData())
->assertRedirect()
@@ -76,21 +73,4 @@ public function testUserCanAssignHisTelegramAccount(): void
$user->refresh();
$this->assertNotNull($user->telegramUser);
}
-
- public function testUserCanDetachHisTelegramAccount(): void
- {
- $user = $this->createUser();
-
- $telegramUser = $user->telegramUser()->save(TelegramUser::factory()->make());
- $this->assertInstanceOf(TelegramUser::class, $telegramUser);
- $this->assertEquals($user->id, $telegramUser->user_id);
-
- $this->actingAs($user)->delete(route('telegram.detach'))->assertRedirect();
-
- $user->refresh();
- $telegramUser->refresh();
-
- $this->assertNull($user->telegramUser);
- $this->assertSoftDeleted($telegramUser);
- }
}
diff --git a/src/tests/Feature/Services/TelegramUserServiceTest.php b/src/tests/Feature/Services/TelegramUserServiceTest.php
new file mode 100644
index 00000000..556b677c
--- /dev/null
+++ b/src/tests/Feature/Services/TelegramUserServiceTest.php
@@ -0,0 +1,61 @@
+telegramUserService = $this->app->make(TelegramUserService::class);
+ }
+
+ public function testUserCannotBeRegisteredTwice(): void
+ {
+ $user = $this->createUserWithTelegramAccount()->telegramUser;
+
+ $this->expectException(TelegramUserException::class);
+ $this->expectExceptionMessage(TelegramUserException::userAlreadyRegistered()->getMessage());
+
+ $this->telegramUserService->register(
+ new TelegramUserDTO(
+ telegramId: $user->telegram_id,
+ firstName : $user->first_name,
+ lastName : $user->last_name,
+ username : $user->username
+ )
+ );
+ }
+
+ public function testRegisteredUserWillHaveTemporaryEmail(): void
+ {
+ $telegramUser = $this->telegramUserService->register(
+ new TelegramUserDTO(
+ telegramId: $this->faker->randomNumber(),
+ firstName : $this->faker->firstName(),
+ lastName : $this->faker->lastName(),
+ username : $this->faker->userName()
+ )
+ );
+
+ $this->assertNotNull($telegramUser->user);
+ $this->assertTrue($telegramUser->user->has_temporary_email);
+ }
+}