# Input Validation Guide

This guide shows how to add validation to API endpoints for production security.

## Current Status

The application currently accepts JSON data without strict validation. For production, add validation constraints using Symfony Validator.

## Installation

Validation component is already included in Symfony 7.3.

## Validation Examples

### 1. RSVP Submission Validation

Create DTO (Data Transfer Object) classes:

**`src/DTO/RsvpSubmissionDto.php`:**
```php
<?php

namespace App\DTO;

use Symfony\Component\Validator\Constraints as Assert;

class RsvpSubmissionDto
{
    #[Assert\NotBlank]
    #[Assert\Length(min: 2, max: 100)]
    public string $firstName;

    #[Assert\NotBlank]
    #[Assert\Length(min: 2, max: 100)]
    public string $lastName;

    #[Assert\Email]
    #[Assert\Length(max: 180)]
    public ?string $email = null;

    #[Assert\Regex(pattern: '/^\+?[0-9\s\-\(\)]+$/', message: 'Invalid phone number format')]
    #[Assert\Length(max: 20)]
    public ?string $phone = null;

    #[Assert\NotBlank]
    #[Assert\Type('array')]
    #[Assert\Count(min: 1, minMessage: 'At least one event must be selected')]
    public array $events = [];

    #[Assert\Length(max: 1000)]
    public ?string $message = null;

    #[Assert\Length(max: 500)]
    public ?string $dietary = null;
}
```

**Update `InviteController.php`:**
```php
use App\DTO\RsvpSubmissionDto;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Validator\Validator\ValidatorInterface;

#[Route('/api/rsvp/public/submit', name: 'api_public_submit', methods: ['POST'])]
public function submitPublicRsvp(
    #[MapRequestPayload] RsvpSubmissionDto $dto,
    Request $request,
    ValidatorInterface $validator
): JsonResponse {
    // Validation happens automatically via MapRequestPayload
    // If validation fails, Symfony returns 422 with error details
    
    // Rate limiting
    $limiter = $this->rsvpSubmitLimiter->create($request->getClientIp());
    if (false === $limiter->consume(1)->isAccepted()) {
        return $this->json(['error' => 'Too many RSVP submissions'], 429);
    }

    // Continue with validated data...
    $data = [
        'firstName' => $dto->firstName,
        'lastName' => $dto->lastName,
        'email' => $dto->email,
        // ... etc
    ];
    
    // Process RSVP
}
```

### 2. Admin API Validation

**`src/DTO/HouseholdDto.php`:**
```php
<?php

namespace App\DTO;

use Symfony\Component\Validator\Constraints as Assert;

class HouseholdDto
{
    #[Assert\NotBlank]
    #[Assert\Length(min: 2, max: 200)]
    public string $displayName;

    #[Assert\Email]
    #[Assert\Length(max: 180)]
    public ?string $email = null;

    #[Assert\Regex(pattern: '/^\+?[0-9\s\-\(\)]+$/', message: 'Invalid phone number')]
    #[Assert\Length(max: 20)]
    public ?string $phone = null;

    #[Assert\Choice(choices: ['BRIDE', 'GROOM', 'BOTH', null])]
    public ?string $side = null;

    #[Assert\Type('array')]
    public ?array $tags = null;

    #[Assert\Length(max: 1000)]
    public ?string $notes = null;
}
```

**`src/DTO/EventDto.php`:**
```php
<?php

namespace App\DTO;

use Symfony\Component\Validator\Constraints as Assert;

class EventDto
{
    #[Assert\NotBlank]
    #[Assert\Length(min: 2, max: 200)]
    public string $name;

    #[Assert\NotBlank]
    #[Assert\Regex(pattern: '/^[a-z0-9\-]+$/', message: 'Slug must be lowercase alphanumeric with dashes')]
    #[Assert\Length(min: 2, max: 100)]
    public string $slug;

    #[Assert\Length(max: 200)]
    public ?string $venueName = null;

    #[Assert\Length(max: 500)]
    public ?string $venueAddress = null;

    #[Assert\Length(max: 2000)]
    public ?string $description = null;

    #[Assert\DateTime]
    public ?string $startAt = null;

    #[Assert\DateTime]
    public ?string $endAt = null;

    #[Assert\Type('bool')]
    public bool $isPublic = false;

    #[Assert\Type('bool')]
    public bool $mealsEnabled = false;

    #[Assert\Type('array')]
    public ?array $mealOptions = null;

    #[Assert\PositiveOrZero]
    public ?int $capacity = null;
}
```

### 3. Settings Validation

**`src/DTO/SettingsDto.php`:**
```php
<?php

namespace App\DTO;

use Symfony\Component\Validator\Constraints as Assert;

class SettingsDto
{
    #[Assert\Choice(choices: ['HOUSEHOLD', 'HOUSEHOLD_WITH_OPTIONAL_NAMES', 'PER_GUEST'])]
    public ?string $publicRsvpMode = null;

    #[Assert\Length(max: 200)]
    public ?string $coupleNames = null;

    #[Assert\DateTime]
    public ?string $weddingDate = null;

    #[Assert\DateTime]
    public ?string $rsvpDeadline = null;

    #[Assert\Length(max: 500)]
    public ?string $locationText = null;

    #[Assert\Regex(pattern: '/^\d{4,6}$/', message: 'PIN must be 4-6 digits')]
    public ?string $staffPin = null;

    #[Assert\Url]
    #[Assert\Length(max: 255)]
    public ?string $baseUrl = null;

    #[Assert\Choice(choices: ['en', 'es', 'fr', 'de'])]
    public ?string $localeDefault = null;

    #[Assert\Range(min: 0, max: 10)]
    public ?int $maxPlusOnesPerHousehold = null;
}
```

**`src/DTO/SmtpSettingsDto.php`:**
```php
<?php

namespace App\DTO;

use Symfony\Component\Validator\Constraints as Assert;

class SmtpSettingsDto
{
    #[Assert\NotBlank(groups: ['smtp_required'])]
    #[Assert\Length(max: 255)]
    public ?string $smtpHost = null;

    #[Assert\NotBlank(groups: ['smtp_required'])]
    #[Assert\Range(min: 1, max: 65535)]
    public ?int $smtpPort = 587;

    #[Assert\NotBlank(groups: ['smtp_required'])]
    #[Assert\Length(max: 255)]
    public ?string $smtpUsername = null;

    #[Assert\NotBlank(groups: ['smtp_required'])]
    #[Assert\Length(max: 255)]
    public ?string $smtpPassword = null;

    #[Assert\Choice(choices: ['tls', 'ssl'])]
    public string $smtpEncryption = 'tls';

    #[Assert\Email]
    #[Assert\Length(max: 255)]
    public ?string $smtpFromEmail = null;

    #[Assert\Length(max: 255)]
    public ?string $smtpFromName = null;
}
```

### 4. Update AdminApiController

**Example with validation:**
```php
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Validator\Validator\ValidatorInterface;

#[Route('/events', methods: ['POST'])]
public function createEvent(
    #[MapRequestPayload] EventDto $eventDto
): JsonResponse {
    // Validation automatic via MapRequestPayload
    // Convert DTO to entity
    $event = new Event();
    $event->setName($eventDto->name);
    $event->setSlug($eventDto->slug);
    $event->setVenueName($eventDto->venueName);
    // ... set other properties
    
    $this->em->persist($event);
    $this->em->flush();

    return $this->json($event, 201);
}
```

## Custom Validation Constraints

For complex validation, create custom constraints:

**`src/Validator/UniqueEventSlug.php`:**
```php
<?php

namespace App\Validator;

use Symfony\Component\Validator\Constraint;

#[\Attribute]
class UniqueEventSlug extends Constraint
{
    public string $message = 'An event with slug "{{ slug }}" already exists.';
}
```

**`src/Validator/UniqueEventSlugValidator.php`:**
```php
<?php

namespace App\Validator;

use App\Repository\EventRepository;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class UniqueEventSlugValidator extends ConstraintValidator
{
    public function __construct(
        private EventRepository $eventRepo
    ) {}

    public function validate($value, Constraint $constraint): void
    {
        if (null === $value || '' === $value) {
            return;
        }

        $existing = $this->eventRepo->findOneBy(['slug' => $value]);
        
        if ($existing) {
            $this->context->buildViolation($constraint->message)
                ->setParameter('{{ slug }}', $value)
                ->addViolation();
        }
    }
}
```

**Use in DTO:**
```php
use App\Validator\UniqueEventSlug;

class EventDto
{
    #[Assert\NotBlank]
    #[UniqueEventSlug]
    public string $slug;
}
```

## Error Handling

Symfony automatically returns 422 responses with validation errors:

```json
{
    "type": "https://symfony.com/errors/validation",
    "title": "Validation Failed",
    "detail": "firstName: This value should not be blank.",
    "violations": [
        {
            "propertyPath": "firstName",
            "title": "This value should not be blank.",
            "parameters": {},
            "type": "urn:uuid:c1051bb4-d103-4f74-8988-acbcafc7fdc3"
        }
    ]
}
```

## Sanitization

For additional security, sanitize user input:

```php
use Symfony\Component\String\Slugger\SluggerInterface;

public function sanitizeInput(string $input, SluggerInterface $slugger): string
{
    // Remove HTML tags
    $input = strip_tags($input);
    
    // Trim whitespace
    $input = trim($input);
    
    // Remove null bytes
    $input = str_replace("\0", '', $input);
    
    return $input;
}
```

## XSS Prevention

Already handled by Twig auto-escaping, but for API responses:

```php
// When outputting user content to JSON
$safeContent = htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
```

## SQL Injection Prevention

Already handled by Doctrine ORM with prepared statements. Never use raw SQL with user input:

```php
// ❌ NEVER DO THIS
$sql = "SELECT * FROM user WHERE email = '" . $email . "'";

// ✅ Always use this
$user = $userRepo->findOneBy(['email' => $email]);

// ✅ Or this for custom queries
$qb = $userRepo->createQueryBuilder('u')
    ->where('u.email = :email')
    ->setParameter('email', $email)
    ->getQuery()
    ->getOneOrNullResult();
```

## Implementation Priority

**High Priority (Do First):**
1. RSVP submission validation (public endpoints)
2. Settings validation
3. Email input validation

**Medium Priority:**
4. Admin API validation (authenticated users)
5. Custom validation constraints

**Low Priority:**
6. Additional sanitization helpers
7. Custom error formatting

## Testing Validation

```bash
# Test with curl
curl -X POST https://yourdomain.com/api/rsvp/public/submit \
  -H "Content-Type: application/json" \
  -d '{"firstName": "", "events": []}'

# Expected: 422 with validation errors
```

## References

- Symfony Validation: https://symfony.com/doc/current/validation.html
- Security Best Practices: https://symfony.com/doc/current/security.html
- OWASP Input Validation: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
