
## API Endpoints Security (Archived Full Guidance)

This document preserves the full guidance previously embedded in `.cursor/rules/api-endpoints-security.mdc`.
The active rule now routes to this archive to reduce default rule-context size and overlap.

**Note:** This file covers security and advanced API endpoint patterns. See also:

- `api-endpoints-core.mdc` - Input validation, error handling, response formats, HubSpot integration, Forms API v3

## Security Guidelines

### CSRF Protection

- Use tokens for form submissions (when possible)
- Verify origin/referer headers
- Rate limiting for repeated submissions

**Pattern:**

```php
// Verify referer (basic CSRF protection)
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$allowed_domains = ['ordio.com', 'www.ordio.com'];
$referer_domain = parse_url($referer, PHP_URL_HOST);

if (!in_array($referer_domain, $allowed_domains)) {
    http_response_code(403);
    echo json_encode(['success' => false, 'message' => 'Invalid request origin.']);
    exit;
}
```

### API Keys

- Store in `v2/config/` directory
- Never commit to git
- Use environment variables when possible
- Rotate keys regularly

### Rate Limiting

**Implement rate limiting:**

```php
// Simple rate limiting (IP-based)
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$rate_limit_file = __DIR__ . '/../logs/rate-limit-' . date('Y-m-d') . '.json';

$rate_limits = [];
if (file_exists($rate_limit_file)) {
    $rate_limits = json_decode(file_get_contents($rate_limit_file), true) ?? [];
}

$ip_requests = $rate_limits[$ip]['count'] ?? 0;
$ip_last_request = $rate_limits[$ip]['last_request'] ?? 0;

// Reset if more than 1 hour ago
if (time() - $ip_last_request > 3600) {
    $ip_requests = 0;
}

// Check limit (e.g., 10 requests per hour)
if ($ip_requests >= 10) {
    http_response_code(429);
    echo json_encode(['success' => false, 'message' => 'Zu viele Anfragen. Bitte versuchen Sie es später erneut.']);
    exit;
}

// Update rate limit
$rate_limits[$ip] = [
    'count' => $ip_requests + 1,
    'last_request' => time()
];
file_put_contents($rate_limit_file, json_encode($rate_limits));
```


## File Generation Specifics

### Excel Generation (`generate_excel.php`)

**Use PhpSpreadsheet library:**

```php
require_once __DIR__ . '/../vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;

$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();

// Set data
$sheet->setCellValue('A1', 'Header 1');
$sheet->setCellValue('B1', 'Header 2');

// Set column widths
$sheet->getColumnDimension('A')->setWidth(25);
$sheet->getColumnDimension('B')->setWidth(20);

// Apply styling (Ordio branding)
$sheet->getStyle('A1:B1')->applyFromArray([
    'font' => ['bold' => true, 'color' => ['rgb' => '4d8ef3']],
    'fill' => ['fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID, 'startColor' => ['rgb' => 'f3f4f6']]
]);

// Generate file
$writer = new Xlsx($spreadsheet);
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename="filename.xlsx"');
$writer->save('php://output');
exit;
```

**Follow Ordio branding:**

- No purple colors
- Use brand colors (blues, neutrals)
- Consistent styling across sheets
- Proper column widths
- No header borders (cleaner look)

### PDF Generation

**Use TCPDF or similar:**

```php
require_once __DIR__ . '/../vendor/tcpdf/tcpdf.php';

$pdf = new TCPDF();
$pdf->SetCreator('Ordio');
$pdf->SetAuthor('Ordio');
$pdf->SetTitle('Document Title');
$pdf->AddPage();
$pdf->WriteHTML($html_content);
$pdf->Output('filename.pdf', 'D'); // D = download
```


## Performance Considerations

### Response Time Targets

- **API response:** < 500ms (target)
- **File generation:** < 2s (acceptable for large files)
- **HubSpot submission:** < 1s (with retry logic)

### Optimization Strategies

**Database queries:**

- Use prepared statements
- Limit result sets
- Cache frequently accessed data

**File operations:**

- Stream large files (don't load into memory)
- Use temporary files for processing
- Clean up temporary files after generation

**External API calls:**

- Use async/background processing when possible
- Implement retry logic with exponential backoff
- Cache responses when appropriate

❌ **BAD:** Long-running operations block response

✅ **GOOD:** Use async processing for heavy tasks (if possible)


## Validation Checklist (API-Specific)

See `.cursor/rules/shared-patterns.mdc` for universal validation checklist.

**API-Specific:**

- [ ] All required fields validated
- [ ] Email format validated (`filter_var`)
- [ ] Input sanitized (`htmlspecialchars`)
- [ ] Error handling with try-catch
- [ ] User-friendly error messages (no internal details)
- [ ] Logging implemented (errors to `logs/` directory)
- [ ] Rate limiting implemented (if applicable)
- [ ] CSRF protection (referer check or tokens)
- [ ] Response format consistent (JSON with `success` field)
- [ ] HTTP status codes correct (200, 400, 500, etc.)
- [ ] Content-Type header set (`application/json`)
- [ ] HubSpot integration tested (if applicable)
- [ ] File generation tested (if applicable)
- [ ] No console.log/error/warn statements


## Common API Pitfalls

### Missing Error Handling

❌ **BAD:**

```php
$result = submitToHubSpot($data);
echo json_encode(['success' => true]);
```

✅ **GOOD:**

```php
try {
    $result = submitToHubSpot($data);
    if ($result['success']) {
        echo json_encode(['success' => true, 'message' => 'Erfolgreich gesendet.']);
    } else {
        throw new Exception('HubSpot submission failed');
    }
} catch (Exception $e) {
    error_log('API Error: ' . $e->getMessage());
    echo json_encode(['success' => false, 'message' => 'Ein Fehler ist aufgetreten.']);
}
```

### Exposing Internal Errors

❌ **BAD:**

```php
catch (Exception $e) {
    echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
```

✅ **GOOD:**

```php
catch (Exception $e) {
    error_log('API Error: ' . $e->getMessage());
    echo json_encode(['success' => false, 'message' => 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.']);
}
```

### Missing Input Validation

❌ **BAD:**

```php
$email = $_POST['email'];
submitToHubSpot(['email' => $email]);
```

✅ **GOOD:**

```php
if (empty($_POST['email']) || !filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
    echo json_encode(['success' => false, 'message' => 'Bitte geben Sie eine gültige E-Mail-Adresse ein.']);
    exit;
}
$email = htmlspecialchars(trim($_POST['email']), ENT_QUOTES, 'UTF-8');
submitToHubSpot(['email' => $email]);
```

### Incorrect Response Format

❌ **BAD:**

```php
echo 'Success';
```

✅ **GOOD:**

```php
header('Content-Type: application/json');
echo json_encode(['success' => true, 'message' => 'Erfolgreich gesendet.']);
```


## Base Form Includes

### HubSpot Form Components

**Files:**

- `v2/base/include_form-hs.php` - Main HubSpot form component
- `v2/base/include_form-hs-sdr.php` - SDR-specific HubSpot form
- `v2/base/include_form-gated-content.php` - Gated content form
- `v2/base/include_form-webinar.php` - Webinar form

**All submit to:** `/form-hs` endpoint (`html/form-hs.php`)

**Required Pattern:**

1. **Hidden Form Field:**

```html
<!-- CRITICAL: Include hubspotutk for Forms API v3 context to link form submission to browser session -->
<input type="hidden" name="hubspotutk" id="hubspotutk" value="" />
```

2. **JavaScript Extraction (before form submission):**

```javascript
// Extract and populate hubspotutk for Forms API v3 context
// Priority: window.utmTracker.getHubspotutk() → cookie fallback
let hubspotutk = "";
if (
  window.utmTracker &&
  typeof window.utmTracker.getHubspotutk === "function"
) {
  hubspotutk = window.utmTracker.getHubspotutk() || "";
} else {
  // Fallback: direct cookie extraction
  const match = document.cookie.match(
    /(?:^|.*;\s*)hubspotutk\s*=\s*([^;]*).*$/
  );
  hubspotutk = match ? decodeURIComponent(match[1]) : "";
}
const hubspotutkField = document.getElementById("hubspotutk");
if (hubspotutkField) {
  hubspotutkField.value = hubspotutk;
}
```

**For `include_form-hs.php` specifically:**

The `populateUTMFields()` function should extract `hubspotutk` from `utmData.hubspotutk` (if available from `getUTMDataForAPI()`), then fallback to direct extraction.

### Salesforce Form Components (No HubSpot Integration)

**Files:**

- `v2/base/include_form.php` - Salesforce form (submits to `/form` with Salesforce org ID)
- `v2/base/include_form-salesforce-sdr.php` - Salesforce SDR form (submits to `/v2/base/salesforce-handler.php`)

**Note:** These forms do NOT integrate with HubSpot and should not include `hubspotutk` extraction.

## Reference Documentation

For detailed workflows:

- `docs/guides/PAGE_TYPE_GUIDES.md` – Understanding API context
- `docs/ai/cursor-playbook.md` – Cursor-specific prompting patterns

## Related Documentation

See [docs/ai/RULE_TO_DOC_MAPPING.md](../../docs/ai/RULE_TO_DOC_MAPPING.md) for complete mapping.
