# 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
