- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
110 lines
2.8 KiB
PHP
110 lines
2.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\MagicLinks\Services;
|
|
|
|
use App\Framework\DateTime\Clock;
|
|
use App\Framework\MagicLinks\MagicLinkData;
|
|
use App\Framework\MagicLinks\MagicLinkToken;
|
|
use App\Framework\MagicLinks\TokenAction;
|
|
use App\Framework\MagicLinks\TokenConfig;
|
|
use App\Framework\MagicLinks\ValueObjects\MagicLinkPayload;
|
|
use App\Framework\Ulid\UlidGenerator;
|
|
|
|
final class InMemoryMagicLinkService implements MagicLinkService
|
|
{
|
|
private array $tokens = [];
|
|
|
|
public function __construct(
|
|
private readonly UlidGenerator $ulidGenerator,
|
|
private readonly Clock $clock
|
|
) {
|
|
}
|
|
|
|
public function generate(
|
|
TokenAction $action,
|
|
array $payload,
|
|
?TokenConfig $config = null,
|
|
?string $createdByIp = null,
|
|
?string $userAgent = null
|
|
): MagicLinkToken {
|
|
$config ??= new TokenConfig();
|
|
$id = $this->ulidGenerator->generate($this->clock);
|
|
;
|
|
$now = new \DateTimeImmutable();
|
|
|
|
$magiclinkData = new MagicLinkData(
|
|
id: $id,
|
|
action: $action,
|
|
payload: MagicLinkPayload::fromArray($payload),
|
|
expiresAt: $config->getExpiryDateTime(),
|
|
createdAt: $now,
|
|
oneTimeUse: $config->oneTimeUse,
|
|
createdByIp: $createdByIp,
|
|
userAgent: $userAgent
|
|
);
|
|
|
|
$token = new MagicLinkToken($id);
|
|
$this->tokens[$id] = $magiclinkData;
|
|
|
|
return $token;
|
|
}
|
|
|
|
public function validate(MagicLinkToken $token): ?MagicLinkData
|
|
{
|
|
if (! isset($this->tokens[$token->value])) {
|
|
return null;
|
|
}
|
|
|
|
$data = $this->tokens[$token->value];
|
|
|
|
if (! $data->isValid()) {
|
|
return null;
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
public function markAsUsed(MagicLinkToken $token): void
|
|
{
|
|
if (isset($this->tokens[$token->value])) {
|
|
$data = $this->tokens[$token->value];
|
|
$this->tokens[$token->value] = $data->withUsed(new \DateTimeImmutable());
|
|
}
|
|
}
|
|
|
|
public function revoke(MagicLinkToken $token): void
|
|
{
|
|
unset($this->tokens[$token->value]);
|
|
}
|
|
|
|
public function exists(MagicLinkToken $token): bool
|
|
{
|
|
return isset($this->tokens[$token->value]) &&
|
|
$this->tokens[$token->value]->isValid();
|
|
}
|
|
|
|
public function getActiveTokens(int $limit = 100): array
|
|
{
|
|
return array_slice(
|
|
array_filter($this->tokens, fn ($data) => $data->isValid()),
|
|
0,
|
|
$limit
|
|
);
|
|
}
|
|
|
|
public function cleanupExpired(): int
|
|
{
|
|
$count = 0;
|
|
foreach ($this->tokens as $id => $data) {
|
|
if ($data->isExpired()) {
|
|
unset($this->tokens[$id]);
|
|
$count++;
|
|
}
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
}
|