Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
79 changes: 79 additions & 0 deletions app/Providers/DomainServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Squidmin\Application\Command\AllowedIp\AddAllowedIpCommandHandler;
use Squidmin\Application\Command\AllowedIp\RemoveAllowedIpCommandHandler;
use Squidmin\Application\Command\SquidUser\CreateSquidUserCommandHandler;
use Squidmin\Application\Command\SquidUser\DeleteSquidUserCommandHandler;
use Squidmin\Application\Command\SquidUser\DisableSquidUserCommandHandler;
use Squidmin\Application\Command\SquidUser\EnableSquidUserCommandHandler;
use Squidmin\Application\Command\SquidUser\ModifySquidUserCommandHandler;
use Squidmin\Application\Command\User\CreateUserCommandHandler;
use Squidmin\Application\Command\User\DeleteUserCommandHandler;
use Squidmin\Application\Command\User\ModifyUserCommandHandler;
use Squidmin\Application\Query\AllowedIp\GetAllowedIpQueryHandler;
use Squidmin\Application\Query\AllowedIp\GetAllowedIpsByOwnerQueryHandler;
use Squidmin\Application\Query\AllowedIp\SearchAllowedIpsQueryHandler;
use Squidmin\Application\Query\SquidUser\GetSquidUserQueryHandler;
use Squidmin\Application\Query\SquidUser\GetSquidUsersByOwnerQueryHandler;
use Squidmin\Application\Query\SquidUser\SearchSquidUsersQueryHandler;
use Squidmin\Application\Query\User\GetAllUsersQueryHandler;
use Squidmin\Application\Query\User\GetUserQueryHandler;
use Squidmin\Application\Query\User\SearchUsersQueryHandler;
use Squidmin\Domain\AllowedIp\AllowedIpRepositoryInterface;
use Squidmin\Domain\SquidUser\SquidUserRepositoryInterface;
use Squidmin\Domain\User\UserRepositoryInterface;
use Squidmin\Infrastructure\Persistence\Eloquent\EloquentAllowedIpRepository;
use Squidmin\Infrastructure\Persistence\Eloquent\EloquentSquidUserRepository;
use Squidmin\Infrastructure\Persistence\Eloquent\EloquentUserRepository;

final class DomainServiceProvider extends ServiceProvider
{
public function register(): void
{
// Register Repositories
$this->app->bind(UserRepositoryInterface::class, EloquentUserRepository::class);
$this->app->bind(SquidUserRepositoryInterface::class, EloquentSquidUserRepository::class);
$this->app->bind(AllowedIpRepositoryInterface::class, EloquentAllowedIpRepository::class);

// Register User Command Handlers
$this->app->bind(CreateUserCommandHandler::class);
$this->app->bind(ModifyUserCommandHandler::class);
$this->app->bind(DeleteUserCommandHandler::class);

// Register User Query Handlers
$this->app->bind(GetUserQueryHandler::class);
$this->app->bind(GetAllUsersQueryHandler::class);
$this->app->bind(SearchUsersQueryHandler::class);

// Register SquidUser Command Handlers
$this->app->bind(CreateSquidUserCommandHandler::class);
$this->app->bind(ModifySquidUserCommandHandler::class);
$this->app->bind(DeleteSquidUserCommandHandler::class);
$this->app->bind(EnableSquidUserCommandHandler::class);
$this->app->bind(DisableSquidUserCommandHandler::class);

// Register SquidUser Query Handlers
$this->app->bind(GetSquidUserQueryHandler::class);
$this->app->bind(GetSquidUsersByOwnerQueryHandler::class);
$this->app->bind(SearchSquidUsersQueryHandler::class);

// Register AllowedIp Command Handlers
$this->app->bind(AddAllowedIpCommandHandler::class);
$this->app->bind(RemoveAllowedIpCommandHandler::class);

// Register AllowedIp Query Handlers
$this->app->bind(GetAllowedIpQueryHandler::class);
$this->app->bind(GetAllowedIpsByOwnerQueryHandler::class);
$this->app->bind(SearchAllowedIpsQueryHandler::class);
}

public function boot(): void
{
//
}
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"autoload": {
"psr-4": {
"App\\": "app/",
"Squidmin\\": "src/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
Expand Down
1 change: 1 addition & 0 deletions config/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\DomainServiceProvider::class,

],

Expand Down
26 changes: 26 additions & 0 deletions src/Application/Command/AllowedIp/AddAllowedIpCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Squidmin\Application\Command\AllowedIp;

use Squidmin\Application\Command\CommandInterface;

final class AddAllowedIpCommand implements CommandInterface
{
public function __construct(
private readonly string $ipAddress,
private readonly int $ownerId
) {
}

public function getIpAddress(): string
{
return $this->ipAddress;
}

public function getOwnerId(): int
{
return $this->ownerId;
}
}
42 changes: 42 additions & 0 deletions src/Application/Command/AllowedIp/AddAllowedIpCommandHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace Squidmin\Application\Command\AllowedIp;

use Squidmin\Application\Command\CommandHandlerInterface;
use Squidmin\Application\Command\CommandInterface;
use Squidmin\Domain\AllowedIp\AllowedIp;
use Squidmin\Domain\AllowedIp\AllowedIpRepositoryInterface;
use Squidmin\Domain\AllowedIp\ValueObject\IpAddress;
use Squidmin\Domain\User\ValueObject\UserId;

final class AddAllowedIpCommandHandler implements CommandHandlerInterface
{
public function __construct(
private readonly AllowedIpRepositoryInterface $allowedIpRepository
) {
}

public function handle(CommandInterface $command): void
{
if (!$command instanceof AddAllowedIpCommand) {
throw new \InvalidArgumentException('Invalid command type');
}

$ipAddress = new IpAddress($command->getIpAddress());
$ownerId = new UserId($command->getOwnerId());

if ($this->allowedIpRepository->existsByIpAndOwnerId($ipAddress, $ownerId)) {
throw new \DomainException('This IP address is already allowed for this user');
}
Comment on lines +30 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

ドメイン例外がグローバル \DomainException になっている
Line 31 の \DomainException は SPL 例外で、Squidmin\Domain\Shared\DomainException 系で統一したい場合に捕捉されません。専用のドメイン例外(例: AllowedIpAlreadyExists)を定義して投げる方が一貫します。

🧩 修正案(専用例外を使用)
 use Squidmin\Domain\AllowedIp\AllowedIp;
 use Squidmin\Domain\AllowedIp\AllowedIpRepositoryInterface;
 use Squidmin\Domain\AllowedIp\ValueObject\IpAddress;
+use Squidmin\Domain\AllowedIp\Exception\AllowedIpAlreadyExists;
 use Squidmin\Domain\User\ValueObject\UserId;
@@
-            throw new \DomainException('This IP address is already allowed for this user');
+            throw new AllowedIpAlreadyExists($ipAddress, $ownerId);

AllowedIpAlreadyExistsSquidmin\Domain\Shared\DomainException を継承するクラスとして新規追加してください。

🤖 Prompt for AI Agents
In `@src/Application/Command/AllowedIp/AddAllowedIpCommandHandler.php` around
lines 30 - 32, Replace the global SPL exception with a domain-specific
exception: create a new AllowedIpAlreadyExists class that extends
Squidmin\Domain\Shared\DomainException, and update AddAllowedIpCommandHandler
(the block that checks
$this->allowedIpRepository->existsByIpAndOwnerId($ipAddress, $ownerId)) to throw
AllowedIpAlreadyExists instead of \DomainException with the same message; ensure
the new exception is namespaced/imported where used so it will be catchable by
domain-level handlers.


$allowedIp = AllowedIp::add(
$this->allowedIpRepository->nextIdentity(),
$ipAddress,
$ownerId
);

$this->allowedIpRepository->save($allowedIp);
}
}
20 changes: 20 additions & 0 deletions src/Application/Command/AllowedIp/RemoveAllowedIpCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Squidmin\Application\Command\AllowedIp;

use Squidmin\Application\Command\CommandInterface;

final class RemoveAllowedIpCommand implements CommandInterface
{
public function __construct(
private readonly int $allowedIpId
) {
}

public function getAllowedIpId(): int
{
return $this->allowedIpId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Squidmin\Application\Command\AllowedIp;

use Squidmin\Application\Command\CommandHandlerInterface;
use Squidmin\Application\Command\CommandInterface;
use Squidmin\Domain\AllowedIp\AllowedIpRepositoryInterface;

final class RemoveAllowedIpCommandHandler implements CommandHandlerInterface
{
public function __construct(
private readonly AllowedIpRepositoryInterface $allowedIpRepository
) {
}

public function handle(CommandInterface $command): void
{
if (!$command instanceof RemoveAllowedIpCommand) {
throw new \InvalidArgumentException('Invalid command type');
}

$allowedIp = $this->allowedIpRepository->findById($command->getAllowedIpId());
if ($allowedIp === null) {
throw new \DomainException('Allowed IP not found');
}
Comment on lines +24 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Allowed IP not found もドメイン例外階層で統一したい
Line 26 の \DomainException は SPL 例外のため、ドメイン例外の捕捉方針とズレます。AllowedIpNotFound のような専用例外へ統一を推奨します。

🧩 修正案(専用例外を使用)
 use Squidmin\Application\Command\CommandHandlerInterface;
 use Squidmin\Application\Command\CommandInterface;
 use Squidmin\Domain\AllowedIp\AllowedIpRepositoryInterface;
+use Squidmin\Domain\AllowedIp\Exception\AllowedIpNotFound;
@@
-            throw new \DomainException('Allowed IP not found');
+            throw new AllowedIpNotFound($command->getAllowedIpId());

AllowedIpNotFoundSquidmin\Domain\Shared\DomainException を継承するクラスとして新規追加してください。

🤖 Prompt for AI Agents
In `@src/Application/Command/AllowedIp/RemoveAllowedIpCommandHandler.php` around
lines 24 - 27, Replace the direct throwing of SPL \DomainException in
RemoveAllowedIpCommandHandler after $this->allowedIpRepository->findById(...)
returns null with a dedicated domain exception class (e.g., AllowedIpNotFound)
that extends Squidmin\Domain\Shared\DomainException; add the new
AllowedIpNotFound exception class and throw that from
RemoveAllowedIpCommandHandler (and update any use/imports) so the handler uses
the domain-specific exception hierarchy.


$allowedIp->remove();
$this->allowedIpRepository->delete($allowedIp);
}
}
10 changes: 10 additions & 0 deletions src/Application/Command/CommandHandlerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Squidmin\Application\Command;

interface CommandHandlerInterface
{
public function handle(CommandInterface $command): void;
}
9 changes: 9 additions & 0 deletions src/Application/Command/CommandInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Squidmin\Application\Command;

interface CommandInterface
{
}
44 changes: 44 additions & 0 deletions src/Application/Command/SquidUser/CreateSquidUserCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Squidmin\Application\Command\SquidUser;

use Squidmin\Application\Command\CommandInterface;

final class CreateSquidUserCommand implements CommandInterface
{
public function __construct(
private readonly string $username,
private readonly string $password,
private readonly int $ownerId,
private readonly ?string $fullname = null,
private readonly ?string $comment = null
) {
}

public function getUsername(): string
{
return $this->username;
}

public function getPassword(): string
{
return $this->password;
}

public function getOwnerId(): int
{
return $this->ownerId;
}

public function getFullname(): ?string
{
return $this->fullname;
}

public function getComment(): ?string
{
return $this->comment;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Squidmin\Application\Command\SquidUser;

use Squidmin\Application\Command\CommandHandlerInterface;
use Squidmin\Application\Command\CommandInterface;
use Squidmin\Domain\SquidUser\SquidUser;
use Squidmin\Domain\SquidUser\SquidUserRepositoryInterface;
use Squidmin\Domain\SquidUser\ValueObject\Comment;
use Squidmin\Domain\SquidUser\ValueObject\Fullname;
use Squidmin\Domain\SquidUser\ValueObject\SquidPassword;
use Squidmin\Domain\SquidUser\ValueObject\SquidUsername;
use Squidmin\Domain\User\ValueObject\UserId;

final class CreateSquidUserCommandHandler implements CommandHandlerInterface
{
public function __construct(
private readonly SquidUserRepositoryInterface $squidUserRepository
) {
}

public function handle(CommandInterface $command): void
{
if (!$command instanceof CreateSquidUserCommand) {
throw new \InvalidArgumentException('Invalid command type');
}

$username = new SquidUsername($command->getUsername());

if ($this->squidUserRepository->existsByUsername($username)) {
throw new \DomainException('Squid user with this username already exists');
}

$squidUser = SquidUser::create(
$this->squidUserRepository->nextIdentity(),
$username,
new SquidPassword($command->getPassword()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd 'SquidPassword.php' -a -x cat -n {}

Repository: 39ff/squid-db-auth-web

Length of output: 1033


🏁 Script executed:

cat -n src/Application/Command/SquidUser/CreateSquidUserCommandHandler.php

Repository: 39ff/squid-db-auth-web

Length of output: 1993


🏁 Script executed:

fd 'SquidUser.php' src/Domain -x cat -n {}

Repository: 39ff/squid-db-auth-web

Length of output: 6160


🏁 Script executed:

rg 'hash|Hash|crypt|password' --type php -i -A 3 -B 1

Repository: 39ff/squid-db-auth-web

Length of output: 50378


SquidPassword クラスでパスワードがハッシュ化されていない重大なセキュリティ脆弱性

SquidPassword クラスは平文のパスワードをそのまま保存しており、ハッシュ化が一切行われていません。EloquentSquidUserRepository でも getValue() の平文がそのままデータベースに保存されます。

コードベースには User クラス向けに HashedPassword という適切に実装された ValueObject が存在します(password_hash()password_verify() を使用)。SquidPassword に対しても同様のパターンを適用する必要があります:

public static function fromPlainPassword(string $plainPassword): self
{
    return new self(password_hash($plainPassword, PASSWORD_DEFAULT));
}

public function verify(string $plainPassword): bool
{
    return password_verify($plainPassword, $this->value);
}
🤖 Prompt for AI Agents
In `@src/Application/Command/SquidUser/CreateSquidUserCommandHandler.php` at line
39, The SquidPassword value object currently stores plain text; update
SquidPassword to hash on construction and add a static constructor and verifier
(e.g., add fromPlainPassword(string): self that uses password_hash and a
verify(string): bool that uses password_verify), then update where it is created
in CreateSquidUserCommandHandler (replace new
SquidPassword($command->getPassword()) with
SquidPassword::fromPlainPassword($command->getPassword())) so
EloquentSquidUserRepository continues to persist SquidPassword->getValue() but
now it contains the hashed password; ensure method names and signatures match
existing usage and tests.

new UserId($command->getOwnerId()),
new Fullname($command->getFullname()),
new Comment($command->getComment())
);

$this->squidUserRepository->save($squidUser);
Comment on lines +30 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

existsByUsernamesave の二段階チェックは競合に弱いです

同時作成で重複をすり抜ける可能性があるため、永続層の一意制約+保存時の例外ハンドリングで原子性を担保してください。

🤖 Prompt for AI Agents
In `@src/Application/Command/SquidUser/CreateSquidUserCommandHandler.php` around
lines 30 - 45, The current two-step check (existsByUsername then save) is
race-prone; make the persistence layer enforce a unique constraint and handle
save-time uniqueness errors atomically: keep the SquidUser::create(...) call but
remove reliance on existsByUsername for correctness, wrap
$this->squidUserRepository->save($squidUser) in a try/catch that catches the
repository/DB unique-constraint exception (e.g.,
UniqueConstraintViolationException or your repository’s specific exception), and
rethrow a DomainException (or translate to a meaningful application exception)
indicating the username already exists; mention existsByUsername, save,
CreateSquidUserCommandHandler and squidUserRepository so you update both
persistence schema and the save error handling in that handler.

}
}
20 changes: 20 additions & 0 deletions src/Application/Command/SquidUser/DeleteSquidUserCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Squidmin\Application\Command\SquidUser;

use Squidmin\Application\Command\CommandInterface;

final class DeleteSquidUserCommand implements CommandInterface
{
public function __construct(
private readonly int $squidUserId
) {
}

public function getSquidUserId(): int
{
return $this->squidUserId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Squidmin\Application\Command\SquidUser;

use Squidmin\Application\Command\CommandHandlerInterface;
use Squidmin\Application\Command\CommandInterface;
use Squidmin\Domain\SquidUser\SquidUserRepositoryInterface;

final class DeleteSquidUserCommandHandler implements CommandHandlerInterface
{
public function __construct(
private readonly SquidUserRepositoryInterface $squidUserRepository
) {
}

public function handle(CommandInterface $command): void
{
if (!$command instanceof DeleteSquidUserCommand) {
throw new \InvalidArgumentException('Invalid command type');
}

$squidUser = $this->squidUserRepository->findById($command->getSquidUserId());
if ($squidUser === null) {
throw new \DomainException('Squid user not found');
}

$squidUser->delete();
$this->squidUserRepository->delete($squidUser);
}
}
Loading