sync: Auto-sync from ACG-M-L5090 at 2026-03-10 19:11:00

Synced files:
- Quote wizard frontend (all components, hooks, types, config)
- API updates (config, models, routers, schemas, services)
- Client work (bg-builders, gurushow)
- Scripts (BGB Lesley termination, CIPP, Datto, migration)
- Temp files (Bardach contacts, VWP investigation, misc)
- Credentials and session logs
- Email service, PHP API, session logs

Machine: ACG-M-L5090
Timestamp: 2026-03-10 19:11:00

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 19:59:08 -07:00
parent a1a19f8c00
commit fa15b03180
169 changed files with 879909 additions and 1243 deletions

View File

@@ -0,0 +1,277 @@
<?php
/**
* Shared utility functions for the MSP Quote Wizard API.
*
* Provides UUID generation, token generation, JSON response helpers,
* input validation, CORS headers, and logging.
*/
// Deny direct access
if (basename($_SERVER['SCRIPT_FILENAME'] ?? '') === basename(__FILE__)) {
http_response_code(403);
exit('Direct access denied.');
}
require_once __DIR__ . '/config.php';
// --------------------------------------------------------------------------
// UUID / Token generation
// --------------------------------------------------------------------------
/**
* Generate a UUID v4 string (lowercase, 36 chars with hyphens).
*
* Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
* where y is one of 8, 9, a, b.
*
* @return string
*/
function generate_uuid(): string
{
$bytes = random_bytes(16);
// Set version to 4 (0100 in binary)
$bytes[6] = chr(ord($bytes[6]) & 0x0f | 0x40);
// Set variant to RFC 4122 (10xx in binary)
$bytes[8] = chr(ord($bytes[8]) & 0x3f | 0x80);
return sprintf(
'%s-%s-%s-%s-%s',
bin2hex(substr($bytes, 0, 4)),
bin2hex(substr($bytes, 4, 2)),
bin2hex(substr($bytes, 6, 2)),
bin2hex(substr($bytes, 8, 2)),
bin2hex(substr($bytes, 10, 6))
);
}
/**
* Generate a URL-safe access token matching Python's secrets.token_urlsafe(32).
*
* Produces a 43-character base64url-encoded string (no padding) from 32
* random bytes, exactly matching the Python implementation.
*
* @return string 43-character URL-safe token
*/
function generate_access_token(): string
{
$bytes = random_bytes(32);
// base64url encode: replace +/ with -_, strip padding =
return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}
// --------------------------------------------------------------------------
// JSON response helpers
// --------------------------------------------------------------------------
/**
* Send a JSON response with the given data and HTTP status code.
*
* Sets Content-Type header, outputs JSON, and terminates the script.
*
* @param mixed $data Data to encode as JSON.
* @param int $status HTTP status code (default 200).
* @return never
*/
function json_response($data, int $status = 200): void
{
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION);
exit;
}
/**
* Send a JSON error response.
*
* @param string $message Error message.
* @param int $status HTTP status code (default 400).
* @param mixed|null $details Additional error details.
* @return never
*/
function error_response(string $message, int $status = 400, $details = null): void
{
$body = ['detail' => $message];
if ($details !== null) {
$body['errors'] = $details;
}
json_response($body, $status);
}
// --------------------------------------------------------------------------
// Request parsing
// --------------------------------------------------------------------------
/**
* Parse the JSON request body.
*
* @return array Decoded JSON as an associative array.
*/
function get_json_body(): array
{
$raw = file_get_contents('php://input');
if (empty($raw)) {
return [];
}
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_response('Invalid JSON in request body', 400);
}
return $data;
}
/**
* Get the client IP address, accounting for reverse proxies.
*
* Checks X-Forwarded-For first, then X-Real-IP, then REMOTE_ADDR.
*
* @return string|null
*/
function get_client_ip(): ?string
{
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
return trim($parts[0]);
}
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
return trim($_SERVER['HTTP_X_REAL_IP']);
}
return $_SERVER['REMOTE_ADDR'] ?? null;
}
/**
* Get the User-Agent header value.
*
* @return string|null
*/
function get_user_agent(): ?string
{
return $_SERVER['HTTP_USER_AGENT'] ?? null;
}
// --------------------------------------------------------------------------
// CORS
// --------------------------------------------------------------------------
/**
* Emit CORS headers based on the configured allowed origins.
*
* For preflight (OPTIONS) requests, this also sets the allowed methods
* and headers, then terminates the script with 204.
*/
function cors_headers(): void
{
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$allowed = array_map('trim', explode(',', CORS_ALLOWED_ORIGINS));
// Allow the origin if it matches our whitelist, or allow all if '*'
if (in_array('*', $allowed, true) || in_array($origin, $allowed, true)) {
$send_origin = in_array('*', $allowed, true) ? '*' : $origin;
header("Access-Control-Allow-Origin: {$send_origin}");
}
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
header('Access-Control-Max-Age: 86400');
// Handle preflight
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
}
// --------------------------------------------------------------------------
// Validation
// --------------------------------------------------------------------------
/**
* Validate that all required fields are present and non-empty in the data.
*
* @param array $data Associative array of input data.
* @param string[] $fields List of required field names.
* @return string[] Array of error messages (empty if valid).
*/
function validate_required(array $data, array $fields): array
{
$errors = [];
foreach ($fields as $field) {
if (!isset($data[$field]) || (is_string($data[$field]) && trim($data[$field]) === '')) {
$errors[] = "Field '{$field}' is required.";
}
}
return $errors;
}
/**
* Validate an email address.
*
* @param string $email Email address to validate.
* @return bool True if valid.
*/
function validate_email(string $email): bool
{
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
// --------------------------------------------------------------------------
// Logging
// --------------------------------------------------------------------------
/**
* Append a message to the application log file.
*
* @param string $level Log level (INFO, WARNING, ERROR).
* @param string $message Log message.
*/
function app_log(string $level, string $message): void
{
$dir = dirname(LOG_FILE);
if (!is_dir($dir)) {
@mkdir($dir, 0750, true);
}
$timestamp = gmdate('Y-m-d\TH:i:s\Z');
$line = "[{$timestamp}] [{$level}] {$message}" . PHP_EOL;
@file_put_contents(LOG_FILE, $line, FILE_APPEND | LOCK_EX);
}
// --------------------------------------------------------------------------
// Datetime helpers
// --------------------------------------------------------------------------
/**
* Format a datetime value for JSON output (ISO 8601 format).
*
* Accepts a datetime string from MySQL (Y-m-d H:i:s) and returns
* an ISO 8601 string, or null if input is null/empty.
*
* @param string|null $dt MySQL datetime string.
* @return string|null ISO 8601 formatted string.
*/
function format_datetime(?string $dt): ?string
{
if ($dt === null || $dt === '' || $dt === '0000-00-00 00:00:00') {
return null;
}
// MySQL DATETIME is already in UTC for this application
$ts = strtotime($dt);
if ($ts === false) {
return null;
}
return gmdate('Y-m-d\TH:i:s\Z', $ts);
}
/**
* Get the current UTC datetime in MySQL format.
*
* @return string Y-m-d H:i:s
*/
function utc_now(): string
{
return gmdate('Y-m-d H:i:s');
}