security: protect API keys with backend proxy, harden auth, htaccess and session management
This commit is contained in:
parent
a77a1a62e7
commit
cde87db064
21 changed files with 190 additions and 1458 deletions
15
.htaccess
15
.htaccess
|
|
@ -2,17 +2,26 @@
|
|||
Options -Indexes
|
||||
RewriteEngine On
|
||||
|
||||
# Interne PHP- und Template-Verzeichnisse vor direktem Webzugriff sperren
|
||||
# HTTP Security Headers
|
||||
<IfModule mod_headers.c>
|
||||
Header always set X-Frame-Options "SAMEORIGIN"
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
</IfModule>
|
||||
|
||||
# Interne PHP-, Vendor- und Template-Verzeichnisse vor direktem Webzugriff sperren
|
||||
RewriteRule ^(home|philcore)/(app|core|views|src)/ - [F,L]
|
||||
RewriteRule ^(.*/)?vendor/ - [F,L]
|
||||
|
||||
# Sensible Dateien und Verzeichnisse vor direktem Webzugriff schützen
|
||||
<FilesMatch "(^\.|\.(json|lock|sql|env|md)$)">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
<Files "auth.php">
|
||||
# Interne Hilfsdateien vor direktem Aufruf schützen
|
||||
<FilesMatch "^(auth\.php|db\.php|google_helper\.php)$">
|
||||
Require all denied
|
||||
</Files>
|
||||
</FilesMatch>
|
||||
|
||||
# 1. Assets aus home/public weiterleiten (falls direkt aufgerufen wie /css/style.css)
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
|
|
|
|||
1
auth.php
1
auth.php
|
|
@ -6,6 +6,7 @@ if (session_status() === PHP_SESSION_NONE) {
|
|||
session_set_cookie_params([
|
||||
'path' => '/',
|
||||
'httponly' => true,
|
||||
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
|
||||
'samesite' => 'Lax' // Lax erlaubt den Wechsel zwischen /home und /test
|
||||
]);
|
||||
session_start();
|
||||
|
|
|
|||
92
dinos/api_proxy.php
Normal file
92
dinos/api_proxy.php
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
// DATEI: dinos/api_proxy.php
|
||||
|
||||
$current_project = 'dinos';
|
||||
require_once __DIR__ . '/../auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Nur POST-Anfragen sind erlaubt.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 1. API-Schlüssel sicher aus .env laden
|
||||
$apiKey = '';
|
||||
$envFiles = [__DIR__ . '/.env', __DIR__ . '/../home/.env'];
|
||||
foreach ($envFiles as $envFile) {
|
||||
if (file_exists($envFile)) {
|
||||
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') continue;
|
||||
if (strpos($line, '=') !== false) {
|
||||
list($k, $v) = explode('=', $line, 2);
|
||||
if (trim($k) === 'GEMINI_API_KEY') {
|
||||
$apiKey = trim(trim($v), '"\'');
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($apiKey)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'API-Schlüssel nicht konfiguriert.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. JSON-Daten empfangen
|
||||
$json_input = file_get_contents('php://input');
|
||||
$request_data = json_decode($json_input, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !isset($request_data['apiUrl']) || !isset($request_data['payload'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Ungültige Anfrage-Daten.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$apiUrl = $request_data['apiUrl'];
|
||||
$payload = $request_data['payload'];
|
||||
|
||||
// 3. SSRF-Schutz: Nur generativelanguage.googleapis.com erlauben
|
||||
$parsed = parse_url($apiUrl);
|
||||
if (!isset($parsed['host']) || $parsed['host'] !== 'generativelanguage.googleapis.com') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Ungültiges API-Ziel.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4. Ziel-URL mit API-Schlüssel zusammensetzen
|
||||
$separator = (strpos($apiUrl, '?') !== false) ? '&' : '?';
|
||||
$fullApiUrl = $apiUrl . $separator . 'key=' . $apiKey;
|
||||
|
||||
// 5. Anfrage mit cURL an Google API senden
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $fullApiUrl,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
],
|
||||
CURLOPT_TIMEOUT => 90,
|
||||
]);
|
||||
|
||||
$response_body = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if (curl_errno($ch)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Fehler bei Weiterleitung an Google: ' . curl_error($ch)]);
|
||||
exit;
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
http_response_code($http_code);
|
||||
echo $response_body;
|
||||
?>
|
||||
|
|
@ -5,6 +5,15 @@
|
|||
* Führt den Google Login durch und speichert den Token.
|
||||
*/
|
||||
|
||||
$current_project = 'dinos';
|
||||
require_once __DIR__ . '/../auth.php';
|
||||
|
||||
// Nur Administratoren dürfen Google OAuth Konten verknüpfen
|
||||
if (!isset($_SESSION['is_admin']) || $_SESSION['is_admin'] !== true) {
|
||||
header('Location: /');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once 'google_helper.php';
|
||||
|
||||
// Konfiguration
|
||||
|
|
|
|||
|
|
@ -16,25 +16,6 @@ $dailyLimitEco = 100; // Eco (Flash) etwa 3-4 Cent pro Bild
|
|||
$dailyLimitPro = 10; // Pro (Imagen 4) etwa 3-4 Cent pro Bild
|
||||
$dailyLimitUltra = 10; // Ultra (Gemini 3) etwa 12 Cent pro Bild
|
||||
|
||||
// API-Key sicher aus .env laden
|
||||
$apiKey = '';
|
||||
$envFiles = [__DIR__ . '/.env', __DIR__ . '/../home/.env'];
|
||||
foreach ($envFiles as $envFile) {
|
||||
if (file_exists($envFile)) {
|
||||
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') continue;
|
||||
if (strpos($line, '=') !== false) {
|
||||
list($k, $v) = explode('=', $line, 2);
|
||||
if (trim($k) === 'GEMINI_API_KEY') {
|
||||
$apiKey = trim(trim($v), '"\'');
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// API Modelle (hier zentral ändern, wenn Versionen veralten) siehe apis.php
|
||||
$modelText = "gemini-2.5-flash"; // Für Fakten und Zufallsgenerator
|
||||
$modelImageEco = "gemini-2.5-flash-image"; // ECO Modus
|
||||
|
|
@ -316,7 +297,6 @@ $totalUltra = $currentStats['total_ultra'];
|
|||
|
||||
<script>
|
||||
// API Konfiguration ans Frontend übergeben
|
||||
const apiKey = "<?php echo $apiKey; ?>";
|
||||
const apiModels = {
|
||||
text: "<?php echo $modelText; ?>",
|
||||
imageEco: "<?php echo $modelImageEco; ?>",
|
||||
|
|
@ -475,11 +455,6 @@ $totalUltra = $currentStats['total_ultra'];
|
|||
|
||||
// --- ZUFALLSGENERATOR LOGIK ---
|
||||
async function handleRandomDino() {
|
||||
if (!apiKey) {
|
||||
showError("API Key fehlt.");
|
||||
return;
|
||||
}
|
||||
|
||||
let diceIconElem = document.getElementById('diceIcon');
|
||||
if (!diceIconElem) diceIconElem = randomBtn.querySelector('svg');
|
||||
|
||||
|
|
@ -488,21 +463,22 @@ $totalUltra = $currentStats['total_ultra'];
|
|||
try {
|
||||
const prompt = DinoPrompts.getRandom();
|
||||
|
||||
const response = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models/${apiModels.text}:generateContent?key=${apiKey}`, {
|
||||
const response = await fetch('api_proxy.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
apiUrl: `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.text}:generateContent`,
|
||||
payload: {
|
||||
contents: [{
|
||||
parts: [{
|
||||
text: prompt
|
||||
}]
|
||||
}]
|
||||
})
|
||||
}
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Netzwerkfehler");
|
||||
const result = await response.json();
|
||||
|
|
@ -551,10 +527,6 @@ $totalUltra = $currentStats['total_ultra'];
|
|||
|
||||
const searchTerm = searchTermInput.value.trim();
|
||||
if (!searchTerm) return;
|
||||
if (!apiKey) {
|
||||
showError("API Key fehlt.");
|
||||
return;
|
||||
}
|
||||
if (typeof DinoPrompts === 'undefined') {
|
||||
showError("Fehler: prompts.js nicht geladen.");
|
||||
return;
|
||||
|
|
@ -667,21 +639,22 @@ $totalUltra = $currentStats['total_ultra'];
|
|||
|
||||
async function fetchDinoFacts(term) {
|
||||
const factsPrompt = DinoPrompts.getFacts(term);
|
||||
const response = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models/${apiModels.text}:generateContent?key=${apiKey}`, {
|
||||
const response = await fetch('api_proxy.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
apiUrl: `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.text}:generateContent`,
|
||||
payload: {
|
||||
contents: [{
|
||||
parts: [{
|
||||
text: factsPrompt
|
||||
}]
|
||||
}]
|
||||
})
|
||||
}
|
||||
);
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
if (response.status === 429 || errText.includes('RESOURCE_EXHAUSTED')) {
|
||||
|
|
@ -708,12 +681,12 @@ $totalUltra = $currentStats['total_ultra'];
|
|||
async function fetchDinoImage(dinoData, type, attempt = 1) {
|
||||
const imagePrompt = DinoPrompts.getImage(dinoData);
|
||||
|
||||
let url = '';
|
||||
let apiUrl = '';
|
||||
let payload = {};
|
||||
let isImagen = false;
|
||||
|
||||
if (type === 'ultra') {
|
||||
url = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imageUltra}:generateContent?key=${apiKey}`;
|
||||
apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imageUltra}:generateContent`;
|
||||
payload = {
|
||||
contents: [{
|
||||
parts: [{
|
||||
|
|
@ -725,7 +698,7 @@ $totalUltra = $currentStats['total_ultra'];
|
|||
}
|
||||
};
|
||||
} else if (type === 'pro') {
|
||||
url = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imagePro}:predict?key=${apiKey}`;
|
||||
apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imagePro}:predict`;
|
||||
payload = {
|
||||
instances: [{
|
||||
prompt: imagePrompt
|
||||
|
|
@ -736,7 +709,7 @@ $totalUltra = $currentStats['total_ultra'];
|
|||
};
|
||||
isImagen = true;
|
||||
} else {
|
||||
url = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imageEco}:generateContent?key=${apiKey}`;
|
||||
apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imageEco}:generateContent`;
|
||||
payload = {
|
||||
contents: [{
|
||||
parts: [{
|
||||
|
|
@ -750,12 +723,15 @@ $totalUltra = $currentStats['total_ultra'];
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const response = await fetch('api_proxy.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
body: JSON.stringify({
|
||||
apiUrl: apiUrl,
|
||||
payload: payload
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
|
|||
1049
dinos/test.html
1049
dinos/test.html
File diff suppressed because it is too large
Load diff
|
|
@ -20,9 +20,11 @@ class LoginController extends Controller {
|
|||
$userModel = $this->model('User');
|
||||
$user = $userModel->authenticate($email, $password);
|
||||
if ($user) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_email'] = $email;
|
||||
$_SESSION['user_projects'] = $user['projects'] ?? [];
|
||||
$_SESSION['is_admin'] = $user['is_admin'] ?? false;
|
||||
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
header('Location: ' . Config::get('BASE_URL', '/'));
|
||||
} else {
|
||||
Flash::set('Access denied: Invalid credentials.');
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ if (session_status() === PHP_SESSION_NONE) {
|
|||
session_set_cookie_params([
|
||||
'path' => '/',
|
||||
'httponly' => true,
|
||||
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
|
||||
'samesite' => 'Lax'
|
||||
]);
|
||||
session_start();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
class Controller {
|
||||
|
||||
public function model($model) {
|
||||
protected function model($model) {
|
||||
$modelPath = __DIR__ . '/../app/models/' . $model . '.php';
|
||||
if (file_exists($modelPath)) {
|
||||
require_once $modelPath;
|
||||
|
|
@ -11,7 +11,7 @@ class Controller {
|
|||
throw new Exception("Model '{$model}' existiert nicht.");
|
||||
}
|
||||
|
||||
public function view($view, $data = []) {
|
||||
protected function view($view, $data = []) {
|
||||
if (!empty($data)) {
|
||||
extract($data);
|
||||
}
|
||||
|
|
@ -25,20 +25,20 @@ class Controller {
|
|||
}
|
||||
}
|
||||
|
||||
public function json($data, $status = 200) {
|
||||
protected function json($data, $status = 200) {
|
||||
http_response_code($status);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function xml($data) {
|
||||
protected function xml($data) {
|
||||
header('Content-Type: application/xml; charset=utf-8');
|
||||
echo $data;
|
||||
exit;
|
||||
}
|
||||
|
||||
public function text($data) {
|
||||
protected function text($data) {
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo $data;
|
||||
exit;
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@ class Router {
|
|||
$this->controller = new $this->controller;
|
||||
// Method Check
|
||||
if (isset($url[1])) {
|
||||
if (method_exists($this->controller, $url[1])) {
|
||||
$this->method = $url[1];
|
||||
$candidateMethod = $url[1];
|
||||
if (is_callable([$this->controller, $candidateMethod]) && strpos($candidateMethod, '_') !== 0) {
|
||||
$this->method = $candidateMethod;
|
||||
unset($url[1]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class Security {
|
|||
}
|
||||
// CSRF Validation
|
||||
public static function checkCsrf($token) {
|
||||
if (!isset($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) {
|
||||
if (!isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], (string)$token)) {
|
||||
throw new Exception("Sicherheits-Token ungültig. Bitte lade die Seite neu.");
|
||||
}
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
if (file_exists(__DIR__ . '/../app/init.php')) {
|
||||
require_once __DIR__ . '/../app/init.php';
|
||||
} else {
|
||||
die("Kritischer Fehler: app/init.php wurde nicht gefunden.");
|
||||
}
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', Config::get('APP_ENV') === 'development' ? 1 : 0);
|
||||
try {
|
||||
// Router
|
||||
$router = new Router();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
<?php
|
||||
// 1. Session starten
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_set_cookie_params([
|
||||
'path' => '/',
|
||||
'httponly' => true,
|
||||
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
|
||||
'samesite' => 'Lax'
|
||||
]);
|
||||
session_start();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
class Controller {
|
||||
|
||||
// Modell laden
|
||||
public function model($model) {
|
||||
protected function model($model) {
|
||||
$modelPath = __DIR__ . '/../app/models/' . $model . '.php';
|
||||
if (file_exists($modelPath)) {
|
||||
require_once $modelPath;
|
||||
|
|
@ -12,7 +12,7 @@ class Controller {
|
|||
}
|
||||
|
||||
// View laden
|
||||
public function view($view, $data = []) {
|
||||
protected function view($view, $data = []) {
|
||||
if (!empty($data)) {
|
||||
extract($data);
|
||||
}
|
||||
|
|
@ -28,10 +28,22 @@ class Controller {
|
|||
}
|
||||
|
||||
// JSON-Response für APIs und AJAX
|
||||
public function json($data, $status = 200) {
|
||||
protected function json($data, $status = 200) {
|
||||
http_response_code($status);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function xml($data) {
|
||||
header('Content-Type: application/xml; charset=utf-8');
|
||||
echo $data;
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function text($data) {
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo $data;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
|
@ -21,8 +21,9 @@ class Router {
|
|||
|
||||
// 2. Methode prüfen (z.B. /user/edit -> Methode edit())
|
||||
if (isset($url[1])) {
|
||||
if (method_exists($this->controller, $url[1])) {
|
||||
$this->method = $url[1];
|
||||
$candidateMethod = $url[1];
|
||||
if (is_callable([$this->controller, $candidateMethod]) && strpos($candidateMethod, '_') !== 0) {
|
||||
$this->method = $candidateMethod;
|
||||
unset($url[1]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class Security {
|
|||
|
||||
// Token beim POST-Request prüfen
|
||||
public static function checkCsrf($token) {
|
||||
if (!isset($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) {
|
||||
if (!isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], (string)$token)) {
|
||||
throw new Exception("Sicherheits-Token ungültig. Bitte lade die Seite neu.");
|
||||
}
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
// Version: 01.10.2025 14:15 (FIX created_at COLUMN)
|
||||
$current_project = 'plants';
|
||||
require_once __DIR__ . '/../auth.php';
|
||||
|
||||
session_start();
|
||||
require_once 'db.php';
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
// Version: 24.09.2025 16:29
|
||||
$current_project = 'plants';
|
||||
require_once __DIR__ . '/../auth.php';
|
||||
|
||||
require_once 'db.php'; // Stellt die $pdo-Verbindung her
|
||||
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Benachrichtigungs-Test</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f0f0f0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 40px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
}
|
||||
p {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
button {
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Test für Browser-Benachrichtigungen</h1>
|
||||
<p>Klicke auf den Knopf, um eine Test-Benachrichtigung auszulösen.</p>
|
||||
<button id="testButton">Benachrichtigung senden</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('testButton').addEventListener('click', async () => {
|
||||
// Schritt 1: Überprüfen und Erlaubnis anfordern
|
||||
if (!('Notification' in window)) {
|
||||
alert('Dieser Browser unterstützt keine Benachrichtigungen.');
|
||||
return;
|
||||
}
|
||||
|
||||
const permission = await Notification.requestPermission();
|
||||
|
||||
if (permission === 'granted') {
|
||||
// Schritt 2: Benachrichtigung erstellen
|
||||
const notification = new Notification('Test-Benachrichtigung', {
|
||||
body: 'Herzlichen Glückwunsch, Benachrichtigungen funktionieren!',
|
||||
icon: 'https://placehold.co/64x64/007bff/FFFFFF?text=OK'
|
||||
});
|
||||
|
||||
// Schritt 3: Event-Listener für den Klick hinzufügen
|
||||
notification.addEventListener('click', () => {
|
||||
window.open('https://google.com', '_blank');
|
||||
});
|
||||
|
||||
console.log('Test-Benachrichtigung gesendet.');
|
||||
} else {
|
||||
alert('Benachrichtigungsberechtigung wurde verweigert.');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
<!-- Version: 24.09.2025 22:10 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Prompt-Vergleich</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 2em;
|
||||
line-height: 1.6;
|
||||
background-color: #f4f4f9;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.panel {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1, h2 {
|
||||
color: #5E8C61;
|
||||
}
|
||||
pre {
|
||||
background-color: #eee;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.loading-bar {
|
||||
width: 0;
|
||||
height: 4px;
|
||||
background-color: #5E8C61;
|
||||
transition: width 0.4s ease;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.controls {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.controls input[type="file"] {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.controls button {
|
||||
padding: 10px 15px;
|
||||
background-color: #5E8C61;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Prompt-Vergleich</h1>
|
||||
<p>Lade ein Bild hoch und vergleiche die Ergebnisse mit dem alten und dem neuen Prompt.</p>
|
||||
|
||||
<div class="controls">
|
||||
<input type="file" id="imageInput" accept="image/*">
|
||||
<button id="runTestBtn">Test starten</button>
|
||||
</div>
|
||||
|
||||
<div id="loadingStatus" style="display: none;">
|
||||
<p>Analysiere mit beiden Prompts...</p>
|
||||
<div class="loading-bar"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="panel">
|
||||
<h2>Alter Prompt</h2>
|
||||
<div id="oldPromptResult"></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>Finaler Profi-Prompt (V6)</h2>
|
||||
<div id="newPromptResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const imageInput = document.getElementById('imageInput');
|
||||
const runTestBtn = document.getElementById('runTestBtn');
|
||||
const loadingStatus = document.getElementById('loadingStatus');
|
||||
const loadingBar = loadingStatus.querySelector('.loading-bar');
|
||||
const oldPromptResult = document.getElementById('oldPromptResult');
|
||||
const newPromptResult = document.getElementById('newPromptResult');
|
||||
|
||||
const PROXY_URL = 'api_proxy.php';
|
||||
|
||||
const oldPrompt = `
|
||||
Du bist ein liebevoller Pflanzenexperte. Analysiere das Bild dieser Pflanze. Gib mir die folgenden Informationen als sauberen JSON-String zurück, ohne zusätzlichen Text oder Markdown.
|
||||
- "name": Der gebräuchliche Name der Pflanze.
|
||||
- "latin_name": Der botanische (lateinische) Name der Pflanze.
|
||||
- "watering_interval_days": Das Gießintervall in Tagen (nur die Zahl).
|
||||
- "fertilizing_interval_days": Das Düngeintervall in Tagen (nur die Zahl).
|
||||
- "care_tips": Gib mir 3 kurze und liebevoll formulierte Pflegetipps.
|
||||
WICHTIGE REGELN FÜR DIE FORMARTIERUNG DER TIPPS:
|
||||
1. Jeder Tipp MUSS mit einem passenden Emoji und einem Titel beginnen (z.B. "☀️ Licht:").
|
||||
2. Zwischen den einzelnen Tipps MUSS sich ein doppelter Zeilenumbruch (\\n\\n) befinden.
|
||||
Falls du die Pflanze nicht sicher erkennen kannst, setze den Wert für "name" und "latin_name" auf "Unbekannt".
|
||||
`;
|
||||
|
||||
const newPrompt = `
|
||||
Du bist ein kritischer Botaniker. Deine Aufgabe ist es, die Pflanze auf dem Bild wissenschaftlich und so exakt wie möglich zu identifizieren.
|
||||
Gib das Ergebnis als sauberen JSON-String zurück, ohne zusätzlichen Text oder Markdown.
|
||||
|
||||
- "name": Der gebräuchlichste Name der Pflanze (nicht zwingend der deutsche).
|
||||
- "latin_name": Der botanische (lateinische) Name der Pflanze.
|
||||
- "plant_family": Die Pflanzenfamilie (z.B. "Araceae").
|
||||
- "plant_genus": Die Pflanzengattung (z.B. "Monstera").
|
||||
- "confidence_score": Bewerte deine Sicherheit bei der Identifizierung KRITISCH und ehrlich auf einer Skala von 0 (geraten) bis 100 (absolut sicher). Sei nicht übermäßig selbstbewusst. Ein Score von 95+ sollte nur bei perfekter Bildqualität und eindeutigen Merkmalen vergeben werden.
|
||||
- "identification_notes": Gib eine sehr kurze Notiz, welche Merkmale zur Identifizierung geführt haben (z.B. "Herzförmige Blätter mit silbernen Flecken").
|
||||
- "watering_interval_days": Das Gießintervall in Tagen (nur die Zahl).
|
||||
- "fertilizing_interval_days": Das Düngeintervall in Tagen (nur die Zahl).
|
||||
- "care_tips": Gib mir 3 kurze Pflegetipps.
|
||||
WICHTIGE REGELN FÜR DIE TIPPS:
|
||||
1. Formuliere die Tipps charmant aus der Ich-Perspektive der Pflanze (z.B. "☀️ Licht: Stell mich an einen hellen Ort, aber ohne direkte Mittagssonne.").
|
||||
2. Jeder Tipp MUSS mit einem passenden Emoji und einem Titel beginnen (z.B. "☀️ Licht:").
|
||||
3. Zwischen den Tipps MUSS sich ein doppelter Zeilenumbruch (\\n\\n) befinden.
|
||||
|
||||
WICHTIG: Falls dein confidence_score unter 75 liegt, setze "name", "latin_name", "plant_family", "plant_genus" auf "Unbekannt" und gib bei "identification_notes" den Grund für die Unsicherheit an (z.B. "Geringe Zuversicht aufgrund von schlechter Bildqualität.").
|
||||
`;
|
||||
|
||||
runTestBtn.addEventListener('click', async () => {
|
||||
const file = imageInput.files[0];
|
||||
if (!file) {
|
||||
alert('Bitte wähle zuerst ein Bild aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
oldPromptResult.innerHTML = '';
|
||||
newPromptResult.innerHTML = '';
|
||||
runTestBtn.disabled = true;
|
||||
loadingStatus.style.display = 'block';
|
||||
loadingBar.style.width = '0%';
|
||||
|
||||
try {
|
||||
const base64Image = await fileToBase64(file);
|
||||
|
||||
const analyzeWithProgress = async (prompt, resultElement) => {
|
||||
try {
|
||||
const result = await analyzeImage(base64Image, prompt);
|
||||
displayResult(resultElement, result);
|
||||
} catch (error) {
|
||||
displayResult(resultElement, { error: error.message });
|
||||
} finally {
|
||||
loadingBar.style.width = (parseInt(loadingBar.style.width) + 50) + '%';
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
analyzeWithProgress(oldPrompt, oldPromptResult),
|
||||
analyzeWithProgress(newPrompt, newPromptResult)
|
||||
]);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test failed:', error);
|
||||
alert('Fehler beim Test: ' + error.message);
|
||||
} finally {
|
||||
runTestBtn.disabled = false;
|
||||
loadingStatus.style.display = 'none';
|
||||
loadingBar.style.width = '0%';
|
||||
}
|
||||
});
|
||||
|
||||
function fileToBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result.split(',')[1]);
|
||||
reader.onerror = error => reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
async function analyzeImage(base64Image, prompt) {
|
||||
const requestPayload = {
|
||||
"contents": [
|
||||
{
|
||||
"parts": [
|
||||
{ "text": prompt },
|
||||
{ "inline_data": { "mime_type": "image/jpeg", "data": base64Image } }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const response = await fetch(PROXY_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ payload: requestPayload })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
throw new Error(`API error! status: ${response.status}, body: ${errorBody}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function displayResult(element, result) {
|
||||
if (result.error) {
|
||||
element.innerHTML = `<p style="color:red;">API Fehler:<br><pre>${result.error}</pre></p>`;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const textResponse = result.candidates[0].content.parts[0].text;
|
||||
const jsonString = textResponse.replace(/```json|```/g, '').trim();
|
||||
const plantData = JSON.parse(jsonString);
|
||||
|
||||
element.innerHTML = `<pre>${JSON.stringify(plantData, null, 2)}</pre>`;
|
||||
} catch (e) {
|
||||
element.innerHTML = `<p style="color:red;">Fehler beim Parsen der Antwort. Rohe Antwort:<br><pre>${JSON.stringify(result, null, 2)}</pre></p>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<?php
|
||||
// DATEI: ./test/index.php
|
||||
|
||||
// 1. Name des Projekts definieren (muss exakt wie im Admin-Dashboard heißen)
|
||||
$current_project = 'test';
|
||||
|
||||
// 2. Zentralen Türsteher aus dem Root einbinden
|
||||
require_once __DIR__ . '/../auth.php';
|
||||
|
||||
// Ab hier ist die Seite sicher!
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Test Projekt</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Zugriff gewährt!</h1>
|
||||
<p>Willkommen im Test-Projekt, <?= htmlspecialchars($_SESSION['user_email']) ?>.</p>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue