Initial commit — Superfice.de website
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
22
api/.htaccess
Normal file
22
api/.htaccess
Normal file
@@ -0,0 +1,22 @@
|
||||
# Block direct access to config file
|
||||
<Files "config.php">
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</Files>
|
||||
|
||||
# CORS headers for API endpoints
|
||||
<IfModule mod_headers.c>
|
||||
Header always set Access-Control-Allow-Origin "https://superfice.de"
|
||||
Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
|
||||
Header always set Access-Control-Allow-Headers "Content-Type"
|
||||
Header always set Access-Control-Max-Age "86400"
|
||||
</IfModule>
|
||||
|
||||
# Handle OPTIONS preflight
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_METHOD} OPTIONS
|
||||
RewriteRule .* - [R=204,L]
|
||||
|
||||
# PHP settings
|
||||
php_flag display_errors Off
|
||||
php_flag log_errors On
|
||||
133
api/contact.php
Normal file
133
api/contact.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
/**
|
||||
* Superfice – Contact Form API
|
||||
*
|
||||
* POST /api/contact.php
|
||||
* Content-Type: application/json
|
||||
*
|
||||
* Body:
|
||||
* {
|
||||
* "name": string (required),
|
||||
* "contact": string (required, email or phone),
|
||||
* "serviceType": string (optional),
|
||||
* "message": string (optional)
|
||||
* }
|
||||
*
|
||||
* Returns JSON: { "success": true }
|
||||
* or { "success": false, "error": "<message>" }
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/helpers.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
cors_headers();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
json_error('Method not allowed', 405);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Parse body
|
||||
// -------------------------------------------------------
|
||||
$raw = file_get_contents('php://input');
|
||||
$body = json_decode($raw, true);
|
||||
|
||||
if (!is_array($body)) {
|
||||
json_error('Ungültige Anfrage.');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Rate limiting
|
||||
// -------------------------------------------------------
|
||||
rate_limit('contact_' . client_ip(), RATE_LIMIT_CONTACT, RATE_LIMIT_WINDOW);
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Validate
|
||||
// -------------------------------------------------------
|
||||
$name = trim($body['name'] ?? '');
|
||||
$contact = trim($body['contact'] ?? '');
|
||||
$serviceType = trim($body['serviceType'] ?? '');
|
||||
$message = trim($body['message'] ?? '');
|
||||
|
||||
if ($name === '') {
|
||||
json_error('Bitte geben Sie Ihren Namen an.');
|
||||
}
|
||||
|
||||
if ($contact === '') {
|
||||
json_error('Bitte geben Sie eine E-Mail-Adresse oder Telefonnummer an.');
|
||||
}
|
||||
|
||||
if (mb_strlen($name) > 200 || mb_strlen($contact) > 300 || mb_strlen($message) > 5000) {
|
||||
json_error('Ein Feld überschreitet die maximale Länge.');
|
||||
}
|
||||
|
||||
// Sanitize
|
||||
$name = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
|
||||
$contact = htmlspecialchars($contact, ENT_QUOTES, 'UTF-8');
|
||||
$serviceType = htmlspecialchars($serviceType, ENT_QUOTES, 'UTF-8');
|
||||
$message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Build e-mail
|
||||
// -------------------------------------------------------
|
||||
$subject = 'Neue Anfrage über superfice.de – ' . ($serviceType ?: 'Allgemein');
|
||||
|
||||
$body_text = <<<TXT
|
||||
Neue Anfrage über superfice.de
|
||||
|
||||
Name: {$name}
|
||||
Kontakt: {$contact}
|
||||
Aufmaß-Typ: {$serviceType}
|
||||
|
||||
Nachricht:
|
||||
{$message}
|
||||
|
||||
---
|
||||
IP: {$_SERVER['REMOTE_ADDR']}
|
||||
Zeit: {$_SERVER['REQUEST_TIME']}
|
||||
TXT;
|
||||
|
||||
$headers = "From: " . MAIL_FROM_NAME . " <" . MAIL_FROM . ">\r\n";
|
||||
$headers .= "Reply-To: {$contact}\r\n";
|
||||
$headers .= "MIME-Version: 1.0\r\n";
|
||||
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
||||
$headers .= "X-Mailer: Superfice-Website/1.0\r\n";
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Send e-mail
|
||||
// -------------------------------------------------------
|
||||
if (USE_SMTP) {
|
||||
send_smtp($subject, $body_text, $headers);
|
||||
} else {
|
||||
$sent = @mail(MAIL_TO, $subject, $body_text, $headers);
|
||||
if (!$sent) {
|
||||
error_log('Superfice contact mail() failed for: ' . $contact);
|
||||
json_error('Beim Senden ist ein Fehler aufgetreten. Bitte schreiben Sie uns direkt an info@superfice.de');
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Optional webhook (Slack / CRM)
|
||||
// -------------------------------------------------------
|
||||
if (defined('WEBHOOK_URL') && WEBHOOK_URL !== '') {
|
||||
$payload = json_encode([
|
||||
'text' => "*Neue Superfice-Anfrage*\nName: {$name}\nKontakt: {$contact}\nTyp: {$serviceType}\nNachricht: {$message}",
|
||||
]);
|
||||
$wh_ctx = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => "Content-Type: application/json\r\n",
|
||||
'content' => $payload,
|
||||
'timeout' => 3,
|
||||
],
|
||||
]);
|
||||
@file_get_contents(WEBHOOK_URL, false, $wh_ctx);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
77
api/distance.php
Normal file
77
api/distance.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/**
|
||||
* Superfice – Distance API
|
||||
*
|
||||
* GET /api/distance.php?plz=XXXXX
|
||||
*
|
||||
* Returns JSON: { "distance": <km>, "plz": "XXXXX" }
|
||||
* or { "error": "<message>" }
|
||||
*
|
||||
* Uses OpenStreetMap Nominatim for geocoding (free, no API key needed).
|
||||
* For production traffic, replace with Google Maps or a local PLZ dataset.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/helpers.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
cors_headers();
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Input validation
|
||||
// -------------------------------------------------------
|
||||
$plz = isset($_GET['plz']) ? preg_replace('/[^0-9]/', '', trim($_GET['plz'])) : '';
|
||||
|
||||
if (strlen($plz) !== 5) {
|
||||
json_error('Ungültige PLZ. Bitte geben Sie eine 5-stellige Postleitzahl ein.');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Rate limiting
|
||||
// -------------------------------------------------------
|
||||
rate_limit('dist_' . client_ip(), RATE_LIMIT_DISTANCE, RATE_LIMIT_WINDOW);
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Geocode PLZ via Nominatim
|
||||
// -------------------------------------------------------
|
||||
$url = sprintf(
|
||||
'https://nominatim.openstreetmap.org/search?postalcode=%s&country=de&format=json&limit=1&addressdetails=0',
|
||||
urlencode($plz)
|
||||
);
|
||||
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'header' => "User-Agent: SuperficeWebsite/1.0 (info@superfice.de)\r\n",
|
||||
'timeout' => 5,
|
||||
'ignore_errors' => true,
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name'=> true,
|
||||
],
|
||||
]);
|
||||
|
||||
$raw = @file_get_contents($url, false, $ctx);
|
||||
|
||||
if ($raw === false) {
|
||||
json_error('PLZ-Suche momentan nicht verfügbar. Bitte versuchen Sie es später erneut.');
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
|
||||
if (empty($data) || !isset($data[0]['lat'], $data[0]['lon'])) {
|
||||
json_error('PLZ nicht gefunden. Bitte prüfen Sie Ihre Eingabe.');
|
||||
}
|
||||
|
||||
$lat = (float) $data[0]['lat'];
|
||||
$lon = (float) $data[0]['lon'];
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Haversine distance to HQ
|
||||
// -------------------------------------------------------
|
||||
$distance = haversine(HQ_LAT, HQ_LON, $lat, $lon);
|
||||
|
||||
echo json_encode([
|
||||
'distance' => $distance,
|
||||
'plz' => $plz,
|
||||
]);
|
||||
112
api/helpers.php
Normal file
112
api/helpers.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
/**
|
||||
* Superfice – API Helpers
|
||||
*
|
||||
* Shared utility functions for API endpoints.
|
||||
* Not directly accessible (no public entry point).
|
||||
*/
|
||||
|
||||
// -------------------------------------------------------
|
||||
// CORS
|
||||
// -------------------------------------------------------
|
||||
function cors_headers(): void {
|
||||
$allowed = defined('SITE_ORIGIN') ? SITE_ORIGIN : '*';
|
||||
header('Access-Control-Allow-Origin: ' . $allowed);
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// JSON error response
|
||||
// -------------------------------------------------------
|
||||
function json_error(string $msg, int $code = 400): never {
|
||||
http_response_code($code);
|
||||
echo json_encode(['error' => $msg]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Client IP (respects proxy headers cautiously)
|
||||
// -------------------------------------------------------
|
||||
function client_ip(): string {
|
||||
// Only trust X-Forwarded-For if behind a known proxy
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
return trim($ips[0]);
|
||||
}
|
||||
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// File-based rate limiting
|
||||
// -------------------------------------------------------
|
||||
function rate_limit(string $key, int $maxRequests, int $window): void {
|
||||
$dir = defined('RATE_LIMIT_DIR') ? RATE_LIMIT_DIR : sys_get_temp_dir() . '/sf_rl';
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0700, true);
|
||||
}
|
||||
|
||||
$file = $dir . '/' . hash('sha256', $key) . '.json';
|
||||
$now = time();
|
||||
$data = ['count' => 0, 'reset' => $now + $window];
|
||||
|
||||
if (file_exists($file)) {
|
||||
$stored = json_decode(file_get_contents($file), true);
|
||||
if (is_array($stored) && $stored['reset'] > $now) {
|
||||
$data = $stored;
|
||||
}
|
||||
}
|
||||
|
||||
$data['count']++;
|
||||
file_put_contents($file, json_encode($data), LOCK_EX);
|
||||
|
||||
if ($data['count'] > $maxRequests) {
|
||||
http_response_code(429);
|
||||
echo json_encode(['error' => 'Zu viele Anfragen. Bitte versuchen Sie es später erneut.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Haversine distance (km)
|
||||
// -------------------------------------------------------
|
||||
function haversine(float $lat1, float $lon1, float $lat2, float $lon2): int {
|
||||
$R = 6371;
|
||||
$dLat = deg2rad($lat2 - $lat1);
|
||||
$dLon = deg2rad($lon2 - $lon1);
|
||||
$a = sin($dLat / 2) ** 2
|
||||
+ cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon / 2) ** 2;
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
return (int) round($R * $c);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// SMTP send (basic, without PHPMailer)
|
||||
// For production, install PHPMailer via Composer and
|
||||
// replace this function body accordingly.
|
||||
// -------------------------------------------------------
|
||||
function send_smtp(string $subject, string $body, string $headers): void {
|
||||
// Placeholder – integrate PHPMailer here:
|
||||
//
|
||||
// require __DIR__ . '/../vendor/autoload.php';
|
||||
// $mail = new PHPMailer\PHPMailer\PHPMailer(true);
|
||||
// $mail->isSMTP();
|
||||
// $mail->Host = SMTP_HOST;
|
||||
// $mail->SMTPAuth = true;
|
||||
// $mail->Username = SMTP_USER;
|
||||
// $mail->Password = SMTP_PASS;
|
||||
// $mail->SMTPSecure = SMTP_SECURE;
|
||||
// $mail->Port = SMTP_PORT;
|
||||
// $mail->setFrom(MAIL_FROM, MAIL_FROM_NAME);
|
||||
// $mail->addAddress(MAIL_TO);
|
||||
// $mail->Subject = $subject;
|
||||
// $mail->Body = $body;
|
||||
// $mail->send();
|
||||
|
||||
// Fallback to mail() until PHPMailer is integrated
|
||||
$sent = @mail(MAIL_TO, $subject, $body, $headers);
|
||||
if (!$sent) {
|
||||
error_log('Superfice SMTP fallback mail() failed');
|
||||
json_error('Beim Senden ist ein Fehler aufgetreten. Bitte schreiben Sie uns direkt.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user