- 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
808 lines
21 KiB
Markdown
808 lines
21 KiB
Markdown
# MagicLinks System
|
|
|
|
Dokumentation des MagicLinks Moduls - Sichere, zeitlich begrenzte Links für verschiedene Aktionen.
|
|
|
|
## Übersicht
|
|
|
|
Das MagicLinks System ermöglicht die Generierung und Verwaltung von sicheren, zeitlich begrenzten Links für verschiedene Anwendungsfälle wie Email-Verifizierung, Passwort-Reset, Dokumentenzugriff und mehr.
|
|
|
|
## Architektur
|
|
|
|
```
|
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
│ Token System │───▶│ Service Layer │───▶│ Action System │
|
|
│ (Value Objects)│ │ (Storage) │ │ (Execution) │
|
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
│ │ │
|
|
MagicLinkToken MagicLinkService MagicLinkAction
|
|
MagicLinkData CacheMagicLink... ActionRegistry
|
|
TokenAction InMemoryMagicLink ActionResult
|
|
TokenConfig
|
|
```
|
|
|
|
## Kern-Komponenten
|
|
|
|
### 1. Token System
|
|
|
|
**SmartLinkToken** - Value Object für Token:
|
|
```php
|
|
final readonly class SmartLinkToken
|
|
{
|
|
public function __construct(
|
|
public string $value // Min. 16 Zeichen
|
|
) {}
|
|
|
|
public function equals(MagicLinkToken $other): bool
|
|
{
|
|
return hash_equals($this->value, $other->value);
|
|
}
|
|
}
|
|
```
|
|
|
|
**MagicLinkData** - Token-Daten:
|
|
```php
|
|
final readonly class MagicLinkData
|
|
{
|
|
public function __construct(
|
|
public string $id,
|
|
public TokenAction $action, // Welche Action
|
|
public array $payload, // Action-spezifische Daten
|
|
public DateTimeImmutable $expiresAt, // Ablaufzeit
|
|
public DateTimeImmutable $createdAt,
|
|
public bool $oneTimeUse = false, // Einmalige Verwendung?
|
|
public ?string $createdByIp = null, // Tracking
|
|
public ?string $userAgent = null,
|
|
public bool $isUsed = false,
|
|
public ?DateTimeImmutable $usedAt = null
|
|
) {}
|
|
|
|
public function isExpired(): bool;
|
|
public function isValid(): bool;
|
|
public function getRemainingTime(): \DateInterval;
|
|
public function withUsed(DateTimeImmutable $usedAt): self;
|
|
}
|
|
```
|
|
|
|
**TokenAction** - Action-Identifier:
|
|
```php
|
|
final readonly class TokenAction
|
|
{
|
|
public function __construct(
|
|
public string $value // z.B. 'email_verification', 'password_reset'
|
|
) {}
|
|
}
|
|
```
|
|
|
|
**TokenConfig** - Token-Konfiguration:
|
|
```php
|
|
final readonly class TokenConfig
|
|
{
|
|
public function __construct(
|
|
public int $ttlSeconds = 3600, // Time-to-Live
|
|
public bool $oneTimeUse = false, // Einmalige Verwendung
|
|
public ?int $maxUses = null, // Max. Anzahl Verwendungen
|
|
public bool $ipRestriction = false // IP-basierte Restriktion
|
|
) {}
|
|
}
|
|
```
|
|
|
|
### 2. Service Layer
|
|
|
|
**MagicLinkService Interface**:
|
|
```php
|
|
interface MagicLinkService
|
|
{
|
|
// Token generieren
|
|
public function generate(
|
|
TokenAction $action,
|
|
array $payload,
|
|
?TokenConfig $config = null,
|
|
?string $createdByIp = null,
|
|
?string $userAgent = null
|
|
): MagicLinkToken;
|
|
|
|
// Token validieren und Daten abrufen
|
|
public function validate(MagicLinkToken $token): ?MagicLinkData;
|
|
|
|
// Token als verwendet markieren
|
|
public function markAsUsed(MagicLinkToken $token): void;
|
|
|
|
// Token widerrufen
|
|
public function revoke(MagicLinkToken $token): void;
|
|
|
|
// Token-Existenz prüfen
|
|
public function exists(MagicLinkToken $token): bool;
|
|
|
|
// Admin: Aktive Tokens abrufen
|
|
public function getActiveTokens(int $limit = 100): array;
|
|
|
|
// Cleanup: Abgelaufene Tokens löschen
|
|
public function cleanupExpired(): int;
|
|
}
|
|
```
|
|
|
|
**Implementierungen**:
|
|
|
|
- **CacheMagicLinkService** - Cache-basierte Implementierung (Production)
|
|
- **InMemoryMagicLinkService** - In-Memory Implementierung (Testing)
|
|
|
|
### 3. Action System
|
|
|
|
**MagicLinkAction Interface**:
|
|
```php
|
|
interface MagicLinkAction
|
|
{
|
|
// Action-Name
|
|
public function getName(): string;
|
|
|
|
// Default-Konfiguration
|
|
public function getDefaultConfig(): TokenConfig;
|
|
|
|
// Payload validieren
|
|
public function validatePayload(array $payload): bool;
|
|
|
|
// Action ausführen
|
|
public function execute(MagicLinkData $magiclinkData, array $context = []): ActionResult;
|
|
|
|
// Erforderliche Permissions
|
|
public function getRequiredPermissions(): array;
|
|
}
|
|
```
|
|
|
|
**ActionResult** - Execution-Ergebnis:
|
|
```php
|
|
final readonly class ActionResult
|
|
{
|
|
public function __construct(
|
|
public bool $success,
|
|
public string $message,
|
|
public array $data = [],
|
|
public array $errors = [],
|
|
public ?string $redirectUrl = null
|
|
) {}
|
|
|
|
public function isSuccess(): bool;
|
|
public function hasRedirect(): bool;
|
|
}
|
|
```
|
|
|
|
**ActionRegistry** - Action-Verwaltung:
|
|
```php
|
|
interface ActionRegistry
|
|
{
|
|
public function register(MagicLinkAction $action): void;
|
|
public function get(TokenAction $action): ?MagicLinkAction;
|
|
public function has(TokenAction $action): bool;
|
|
public function all(): array;
|
|
}
|
|
```
|
|
|
|
### 4. Vordefinierte Actions
|
|
|
|
**EmailVerificationAction**:
|
|
```php
|
|
final readonly class EmailVerificationAction implements MagicLinkAction
|
|
{
|
|
public function getName(): string
|
|
{
|
|
return 'email_verification';
|
|
}
|
|
|
|
public function getDefaultConfig(): TokenConfig
|
|
{
|
|
return new TokenConfig(
|
|
ttlSeconds: 86400, // 24 Stunden
|
|
oneTimeUse: true // Einmalig verwendbar
|
|
);
|
|
}
|
|
|
|
public function validatePayload(array $payload): bool
|
|
{
|
|
return isset($payload['user_id']) && isset($payload['email']);
|
|
}
|
|
|
|
public function execute(MagicLinkData $magiclinkData, array $context = []): ActionResult
|
|
{
|
|
// Email-Verifizierungs-Logik
|
|
}
|
|
}
|
|
```
|
|
|
|
**PasswordResetAction**:
|
|
```php
|
|
final readonly class PasswordResetAction implements MagicLinkAction
|
|
{
|
|
public function getName(): string
|
|
{
|
|
return 'password_reset';
|
|
}
|
|
|
|
public function getDefaultConfig(): TokenConfig
|
|
{
|
|
return new TokenConfig(
|
|
ttlSeconds: 3600, // 1 Stunde
|
|
oneTimeUse: true,
|
|
ipRestriction: true // IP-gebunden
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
**DocumentAccessAction** - Temporärer Dokumentenzugriff:
|
|
```php
|
|
final readonly class DocumentAccessAction implements MagicLinkAction
|
|
{
|
|
public function getName(): string
|
|
{
|
|
return 'document_access';
|
|
}
|
|
|
|
public function getDefaultConfig(): TokenConfig
|
|
{
|
|
return new TokenConfig(
|
|
ttlSeconds: 3600, // 1 Stunde
|
|
maxUses: 5 // Max. 5 Downloads
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
**GenericDataAccessAction** - Generischer Datenzugriff:
|
|
```php
|
|
final readonly class GenericDataAccessAction implements MagicLinkAction
|
|
{
|
|
public function getName(): string
|
|
{
|
|
return 'data_access';
|
|
}
|
|
}
|
|
```
|
|
|
|
## Command/Handler Pattern
|
|
|
|
**GenerateMagicLinkCommand**:
|
|
```php
|
|
final readonly class GenerateMagicLinkCommand
|
|
{
|
|
public function __construct(
|
|
public TokenAction $action,
|
|
public array $payload,
|
|
public string $baseUrl,
|
|
public ?TokenConfig $config = null
|
|
) {}
|
|
}
|
|
```
|
|
|
|
**GenerateMagicLinkHandler**:
|
|
```php
|
|
final readonly class GenerateMagicLinkHandler
|
|
{
|
|
public function handle(GenerateMagicLinkCommand $command): string
|
|
{
|
|
// 1. Token generieren
|
|
$token = $this->magiclinkService->generate(
|
|
$command->action,
|
|
$command->payload,
|
|
$command->config
|
|
);
|
|
|
|
// 2. URL zusammenbauen
|
|
return $command->baseUrl . '/magiclink/' . $token->value;
|
|
}
|
|
}
|
|
```
|
|
|
|
**ExecuteMagicLinkCommand**:
|
|
```php
|
|
final readonly class ExecuteMagicLinkCommand
|
|
{
|
|
public function __construct(
|
|
public MagicLinkToken $token,
|
|
public array $context = []
|
|
) {}
|
|
}
|
|
```
|
|
|
|
**ExecuteMagicLinkHandler**:
|
|
```php
|
|
final readonly class ExecuteMagicLinkHandler
|
|
{
|
|
public function handle(ExecuteMagicLinkCommand $command): ActionResult
|
|
{
|
|
// 1. Token validieren
|
|
$data = $this->magiclinkService->validate($command->token);
|
|
|
|
// 2. Action holen und ausführen
|
|
$action = $this->actionRegistry->get($data->action);
|
|
$result = $action->execute($data, $command->context);
|
|
|
|
// 3. Bei One-Time-Use: Als verwendet markieren
|
|
if ($data->oneTimeUse && $result->isSuccess()) {
|
|
$this->magiclinkService->markAsUsed($command->token);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|
|
```
|
|
|
|
## HTTP Controller
|
|
|
|
**Route**: `/magiclink/{token}` (GET/POST)
|
|
|
|
```php
|
|
final readonly class MagicLink
|
|
{
|
|
#[Route('/magiclink/{token}', method: Method::GET)]
|
|
#[Route('/magiclink/{token}', method: Method::POST)]
|
|
public function execute(string $token, Request $request): ActionResult
|
|
{
|
|
// 1. Token validieren
|
|
$magiclinkToken = new MagicLinkToken($token);
|
|
$magiclinkData = $this->magiclinkService->validate($magiclinkToken);
|
|
|
|
if (!$magiclinkData) {
|
|
return new ViewResult(
|
|
template: 'magiclinks-error',
|
|
data: ['error' => 'Ungültiger oder abgelaufener Link']
|
|
);
|
|
}
|
|
|
|
// 2. Action holen
|
|
$action = $this->actionRegistry->get($magiclinkData->action);
|
|
|
|
// 3. Context vorbereiten
|
|
$context = [
|
|
'request_method' => $request->method->value,
|
|
'request_data' => $request->parsedBody ?? [],
|
|
'query_params' => $request->queryParameters ?? [],
|
|
'user_agent' => $request->headers->getFirst(HeaderKey::USER_AGENT),
|
|
];
|
|
|
|
// 4. Command ausführen
|
|
$command = new ExecuteMagicLinkCommand($magiclinkToken, $context);
|
|
$result = $this->commandBus->dispatch($command);
|
|
|
|
// 5. Ergebnis verarbeiten
|
|
if ($result->hasRedirect()) {
|
|
return new Redirect($result->redirectUrl);
|
|
}
|
|
|
|
return new ViewResult(
|
|
template: $action->getViewTemplate(),
|
|
data: ['result' => $result]
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Verwendung
|
|
|
|
### MagicLink generieren
|
|
|
|
```php
|
|
use App\Framework\MagicLinks\Commands\GenerateMagicLinkCommand;
|
|
use App\Framework\MagicLinks\TokenAction;
|
|
use App\Framework\MagicLinks\TokenConfig;
|
|
|
|
// 1. Command erstellen
|
|
$command = new GenerateMagicLinkCommand(
|
|
action: new TokenAction('email_verification'),
|
|
payload: [
|
|
'user_id' => 123,
|
|
'email' => 'user@example.com'
|
|
],
|
|
baseUrl: 'https://example.com',
|
|
config: new TokenConfig(
|
|
ttlSeconds: 86400, // 24 Stunden
|
|
oneTimeUse: true
|
|
)
|
|
);
|
|
|
|
// 2. Via CommandBus dispatchen
|
|
$url = $this->commandBus->dispatch($command);
|
|
|
|
// 3. URL versenden (z.B. per Email)
|
|
$this->mailer->send($user->email, $url);
|
|
```
|
|
|
|
### Eigene Action implementieren
|
|
|
|
```php
|
|
final readonly class InvitationAcceptAction implements MagicLinkAction
|
|
{
|
|
public function getName(): string
|
|
{
|
|
return 'invitation_accept';
|
|
}
|
|
|
|
public function getDefaultConfig(): TokenConfig
|
|
{
|
|
return new TokenConfig(
|
|
ttlSeconds: 604800, // 7 Tage
|
|
oneTimeUse: true
|
|
);
|
|
}
|
|
|
|
public function validatePayload(array $payload): bool
|
|
{
|
|
return isset($payload['invitation_id'])
|
|
&& isset($payload['invitee_email']);
|
|
}
|
|
|
|
public function execute(MagicLinkData $magiclinkData, array $context = []): ActionResult
|
|
{
|
|
$invitationId = $magiclinkData->payload['invitation_id'];
|
|
|
|
try {
|
|
// Business Logic
|
|
$this->invitationService->accept($invitationId);
|
|
|
|
return new ActionResult(
|
|
success: true,
|
|
message: 'Einladung erfolgreich angenommen',
|
|
redirectUrl: '/dashboard'
|
|
);
|
|
} catch (\Exception $e) {
|
|
return new ActionResult(
|
|
success: false,
|
|
message: 'Fehler beim Annehmen der Einladung',
|
|
errors: [$e->getMessage()]
|
|
);
|
|
}
|
|
}
|
|
|
|
public function getRequiredPermissions(): array
|
|
{
|
|
return []; // Öffentlich zugänglich
|
|
}
|
|
}
|
|
```
|
|
|
|
### Action registrieren
|
|
|
|
```php
|
|
// In MagicLinkInitializer
|
|
final readonly class MagicLinkInitializer
|
|
{
|
|
#[Initializer]
|
|
public function __invoke(): ActionRegistry
|
|
{
|
|
$registry = new DefaultActionRegistry();
|
|
|
|
// Vordefinierte Actions
|
|
$registry->register(new EmailVerificationAction());
|
|
$registry->register(new PasswordResetAction());
|
|
$registry->register(new DocumentAccessAction());
|
|
|
|
// Custom Action
|
|
$registry->register(new InvitationAcceptAction());
|
|
|
|
return $registry;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Service-Konfiguration
|
|
|
|
```php
|
|
// In MagicLinkInitializer
|
|
#[Initializer]
|
|
public function init(): MagicLinkService
|
|
{
|
|
// Production: Cache-basiert
|
|
return $this->container->get(CacheMagicLinkService::class);
|
|
|
|
// Testing: In-Memory
|
|
// return $this->container->get(InMemoryMagicLinkService::class);
|
|
}
|
|
```
|
|
|
|
## Sicherheitsaspekte
|
|
|
|
### Token-Generierung
|
|
- **Min. 16 Zeichen** für Token-Sicherheit
|
|
- **Kryptographisch sichere** Zufallstoken
|
|
- **Hash-basierter Vergleich** via `hash_equals()`
|
|
|
|
### Token-Validierung
|
|
- **Ablaufzeit-Prüfung** - `isExpired()`
|
|
- **One-Time-Use** - `isUsed` Flag
|
|
- **Max-Uses Tracking** - Optional
|
|
- **IP-Restriktion** - Optional via `createdByIp`
|
|
|
|
### Action-Ausführung
|
|
- **Payload-Validierung** - `validatePayload()`
|
|
- **Permission-Checks** - `getRequiredPermissions()`
|
|
- **Context-Validierung** - Request-Daten prüfen
|
|
- **Error-Handling** - Graceful Degradation
|
|
|
|
## Cleanup & Wartung
|
|
|
|
### Automatisches Cleanup
|
|
|
|
```php
|
|
// Cleanup-Command erstellen
|
|
final readonly class CleanupExpiredMagicLinksCommand implements ConsoleCommand
|
|
{
|
|
public function __construct(
|
|
private MagicLinkService $magiclinkService
|
|
) {}
|
|
|
|
public function execute(ConsoleInput $input): int
|
|
{
|
|
$deleted = $this->magiclinkService->cleanupExpired();
|
|
|
|
echo "Deleted {$deleted} expired magiclinks\n";
|
|
|
|
return ExitCode::SUCCESS;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Scheduled Cleanup via Scheduler
|
|
|
|
```php
|
|
// In Scheduler registrieren
|
|
$scheduler->schedule(
|
|
'cleanup-magiclinks',
|
|
CronSchedule::fromExpression('0 2 * * *'), // Täglich 02:00
|
|
fn() => $this->magiclinkService->cleanupExpired()
|
|
);
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Unit Tests
|
|
|
|
```php
|
|
describe('MagicLinkService', function () {
|
|
it('generates valid tokens', function () {
|
|
$service = new InMemoryMagicLinkService();
|
|
|
|
$token = $service->generate(
|
|
action: new TokenAction('test'),
|
|
payload: ['user_id' => 1]
|
|
);
|
|
|
|
expect($token->value)->toHaveLength(32);
|
|
expect($service->exists($token))->toBeTrue();
|
|
});
|
|
|
|
it('validates tokens correctly', function () {
|
|
$service = new InMemorySmartLinkService();
|
|
|
|
$token = $service->generate(
|
|
action: new TokenAction('test'),
|
|
payload: ['data' => 'test'],
|
|
config: new TokenConfig(ttlSeconds: 3600)
|
|
);
|
|
|
|
$data = $service->validate($token);
|
|
|
|
expect($data)->not->toBeNull();
|
|
expect($data->payload['data'])->toBe('test');
|
|
expect($data->isValid())->toBeTrue();
|
|
});
|
|
|
|
it('marks one-time tokens as used', function () {
|
|
$service = new InMemorySmartLinkService();
|
|
|
|
$token = $service->generate(
|
|
action: new TokenAction('test'),
|
|
payload: [],
|
|
config: new TokenConfig(oneTimeUse: true)
|
|
);
|
|
|
|
$service->markAsUsed($token);
|
|
|
|
$data = $service->validate($token);
|
|
expect($data->isValid())->toBeFalse();
|
|
});
|
|
});
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
```php
|
|
describe('MagicLink Controller', function () {
|
|
it('executes valid magiclink', function () {
|
|
// Token generieren
|
|
$command = new GenerateMagicLinkCommand(
|
|
action: new TokenAction('email_verification'),
|
|
payload: ['user_id' => 1, 'email' => 'test@example.com'],
|
|
baseUrl: 'https://test.local'
|
|
);
|
|
|
|
$url = $this->commandBus->dispatch($command);
|
|
|
|
// Request simulieren
|
|
$response = $this->get($url);
|
|
|
|
expect($response->status)->toBe(Status::OK);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### 1. Token-Konfiguration
|
|
- **Email-Verifizierung**: 24h TTL, One-Time-Use
|
|
- **Passwort-Reset**: 1h TTL, One-Time-Use, IP-Restriction
|
|
- **Dokumentenzugriff**: 1h TTL, Max-Uses basierend auf Bedarf
|
|
- **Einladungen**: 7d TTL, One-Time-Use
|
|
|
|
### 2. Payload-Design
|
|
- **Minimale Daten**: Nur notwendige IDs, keine sensiblen Daten
|
|
- **Validierung**: Immer `validatePayload()` implementieren
|
|
- **Versionierung**: Bei Schema-Änderungen Versionsnummer in Payload
|
|
|
|
### 3. Action-Implementation
|
|
- **Idempotenz**: Actions sollten mehrfach ausführbar sein
|
|
- **Atomarität**: Entweder vollständig erfolgreich oder vollständig fehlgeschlagen
|
|
- **Logging**: Wichtige Actions loggen für Audit-Trail
|
|
|
|
### 4. Error-Handling
|
|
- **Graceful Degradation**: Sinnvolle Fehlermeldungen
|
|
- **User-Feedback**: Klare Kommunikation bei Problemen
|
|
- **Security**: Keine internen Details in Fehlermeldungen
|
|
|
|
### 5. Monitoring
|
|
- **Token-Usage**: Tracking von Token-Generierung und -Verwendung
|
|
- **Action-Success-Rate**: Erfolgsrate verschiedener Actions
|
|
- **Expired-Tokens**: Anzahl abgelaufener Tokens monitoren
|
|
- **Cleanup-Effizienz**: Performance des Cleanup-Prozesses
|
|
|
|
## Troubleshooting
|
|
|
|
### Problem: Token wird nicht validiert
|
|
|
|
**Ursachen**:
|
|
- Token abgelaufen
|
|
- Token bereits verwendet (One-Time-Use)
|
|
- Token wurde widerrufen
|
|
- Cache-Inkonsistenz
|
|
|
|
**Lösung**:
|
|
```php
|
|
$data = $service->validate($token);
|
|
|
|
if (!$data) {
|
|
// Token existiert nicht
|
|
} elseif ($data->isExpired()) {
|
|
// Token abgelaufen
|
|
} elseif ($data->isUsed) {
|
|
// Token bereits verwendet
|
|
}
|
|
```
|
|
|
|
### Problem: Action schlägt fehl
|
|
|
|
**Ursachen**:
|
|
- Payload-Validierung fehlgeschlagen
|
|
- Business-Logic-Fehler
|
|
- Fehlende Permissions
|
|
|
|
**Lösung**:
|
|
```php
|
|
if (!$action->validatePayload($data->payload)) {
|
|
return new ActionResult(
|
|
success: false,
|
|
message: 'Ungültige Token-Daten',
|
|
errors: ['Invalid payload structure']
|
|
);
|
|
}
|
|
```
|
|
|
|
### Problem: Performance bei vielen Tokens
|
|
|
|
**Lösungen**:
|
|
- **Regelmäßiges Cleanup**: Expired Tokens entfernen
|
|
- **Index-Optimierung**: Bei Database-backed Service
|
|
- **Cache-Strategie**: Richtige Cache-TTL wählen
|
|
- **Batch-Operations**: Cleanup in Batches
|
|
|
|
## Framework-Integration
|
|
|
|
### Mit Event System
|
|
|
|
```php
|
|
// Event bei Token-Generierung
|
|
$this->eventDispatcher->dispatch(
|
|
new MagicLinkGeneratedEvent($token, $action)
|
|
);
|
|
|
|
// Event bei Action-Ausführung
|
|
$this->eventDispatcher->dispatch(
|
|
new MagicLinkExecutedEvent($token, $result)
|
|
);
|
|
```
|
|
|
|
### Mit Queue System
|
|
|
|
```php
|
|
// Asynchrone Action-Ausführung
|
|
final readonly class MagicLinkExecutionJob
|
|
{
|
|
public function handle(): void
|
|
{
|
|
$command = new ExecuteMagicLinkCommand($this->token);
|
|
$this->commandBus->dispatch($command);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Mit Scheduler
|
|
|
|
```php
|
|
// Regelmäßiges Cleanup
|
|
$scheduler->schedule(
|
|
'magiclink-cleanup',
|
|
IntervalSchedule::every(Duration::fromHours(6)),
|
|
fn() => $this->magiclinkService->cleanupExpired()
|
|
);
|
|
```
|
|
|
|
## Erweiterungen
|
|
|
|
### Multi-Factor MagicLinks
|
|
|
|
```php
|
|
final readonly class MfaRequiredAction implements MagicLinkAction
|
|
{
|
|
public function execute(MagicLinkData $data, array $context = []): ActionResult
|
|
{
|
|
// Zusätzliche MFA-Verifikation anfordern
|
|
if (!isset($context['mfa_code'])) {
|
|
return new ActionResult(
|
|
success: false,
|
|
message: 'MFA-Code erforderlich',
|
|
data: ['requires_mfa' => true]
|
|
);
|
|
}
|
|
|
|
// MFA validieren und Action ausführen
|
|
}
|
|
}
|
|
```
|
|
|
|
### Rate-Limited Actions
|
|
|
|
```php
|
|
final readonly class RateLimitedAction implements MagicLinkAction
|
|
{
|
|
public function execute(MagicLinkData $data, array $context = []): ActionResult
|
|
{
|
|
$ipAddress = $context['ip_address'];
|
|
|
|
if ($this->rateLimiter->tooManyAttempts($ipAddress, 5, 3600)) {
|
|
return new ActionResult(
|
|
success: false,
|
|
message: 'Zu viele Versuche'
|
|
);
|
|
}
|
|
|
|
// Action ausführen
|
|
}
|
|
}
|
|
```
|
|
|
|
## Zusammenfassung
|
|
|
|
Das MagicLinks System bietet:
|
|
- ✅ **Sichere Token-Generierung** mit konfigurierbarer TTL
|
|
- ✅ **Flexible Action-System** für verschiedene Use-Cases
|
|
- ✅ **Command/Handler Pattern** für saubere Architektur
|
|
- ✅ **One-Time-Use & Max-Uses** Support
|
|
- ✅ **IP-Restriction** und Security-Features
|
|
- ✅ **Automatisches Cleanup** abgelaufener Tokens
|
|
- ✅ **Framework-Integration** mit Events, Queue, Scheduler
|
|
- ✅ **Testbare Architektur** mit In-Memory Implementation
|
|
|
|
Das System folgt konsequent Framework-Patterns:
|
|
- **Value Objects** für Token, Action, Config
|
|
- **Readonly Classes** für Unveränderlichkeit
|
|
- **Interface-Driven** für Flexibilität
|
|
- **Command/Handler** für Business Logic
|
|
- **Dependency Injection** für Testbarkeit
|