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
|
Options -Indexes
|
||||||
RewriteEngine On
|
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 ^(home|philcore)/(app|core|views|src)/ - [F,L]
|
||||||
|
RewriteRule ^(.*/)?vendor/ - [F,L]
|
||||||
|
|
||||||
# Sensible Dateien und Verzeichnisse vor direktem Webzugriff schützen
|
# Sensible Dateien und Verzeichnisse vor direktem Webzugriff schützen
|
||||||
<FilesMatch "(^\.|\.(json|lock|sql|env|md)$)">
|
<FilesMatch "(^\.|\.(json|lock|sql|env|md)$)">
|
||||||
Require all denied
|
Require all denied
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|
||||||
<Files "auth.php">
|
# Interne Hilfsdateien vor direktem Aufruf schützen
|
||||||
|
<FilesMatch "^(auth\.php|db\.php|google_helper\.php)$">
|
||||||
Require all denied
|
Require all denied
|
||||||
</Files>
|
</FilesMatch>
|
||||||
|
|
||||||
# 1. Assets aus home/public weiterleiten (falls direkt aufgerufen wie /css/style.css)
|
# 1. Assets aus home/public weiterleiten (falls direkt aufgerufen wie /css/style.css)
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
|
|
||||||
1
auth.php
1
auth.php
|
|
@ -6,6 +6,7 @@ if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_set_cookie_params([
|
session_set_cookie_params([
|
||||||
'path' => '/',
|
'path' => '/',
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
|
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
|
||||||
'samesite' => 'Lax' // Lax erlaubt den Wechsel zwischen /home und /test
|
'samesite' => 'Lax' // Lax erlaubt den Wechsel zwischen /home und /test
|
||||||
]);
|
]);
|
||||||
session_start();
|
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.
|
* 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';
|
require_once 'google_helper.php';
|
||||||
|
|
||||||
// Konfiguration
|
// 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
|
$dailyLimitPro = 10; // Pro (Imagen 4) etwa 3-4 Cent pro Bild
|
||||||
$dailyLimitUltra = 10; // Ultra (Gemini 3) etwa 12 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
|
// API Modelle (hier zentral ändern, wenn Versionen veralten) siehe apis.php
|
||||||
$modelText = "gemini-2.5-flash"; // Für Fakten und Zufallsgenerator
|
$modelText = "gemini-2.5-flash"; // Für Fakten und Zufallsgenerator
|
||||||
$modelImageEco = "gemini-2.5-flash-image"; // ECO Modus
|
$modelImageEco = "gemini-2.5-flash-image"; // ECO Modus
|
||||||
|
|
@ -316,7 +297,6 @@ $totalUltra = $currentStats['total_ultra'];
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// API Konfiguration ans Frontend übergeben
|
// API Konfiguration ans Frontend übergeben
|
||||||
const apiKey = "<?php echo $apiKey; ?>";
|
|
||||||
const apiModels = {
|
const apiModels = {
|
||||||
text: "<?php echo $modelText; ?>",
|
text: "<?php echo $modelText; ?>",
|
||||||
imageEco: "<?php echo $modelImageEco; ?>",
|
imageEco: "<?php echo $modelImageEco; ?>",
|
||||||
|
|
@ -475,11 +455,6 @@ $totalUltra = $currentStats['total_ultra'];
|
||||||
|
|
||||||
// --- ZUFALLSGENERATOR LOGIK ---
|
// --- ZUFALLSGENERATOR LOGIK ---
|
||||||
async function handleRandomDino() {
|
async function handleRandomDino() {
|
||||||
if (!apiKey) {
|
|
||||||
showError("API Key fehlt.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let diceIconElem = document.getElementById('diceIcon');
|
let diceIconElem = document.getElementById('diceIcon');
|
||||||
if (!diceIconElem) diceIconElem = randomBtn.querySelector('svg');
|
if (!diceIconElem) diceIconElem = randomBtn.querySelector('svg');
|
||||||
|
|
||||||
|
|
@ -488,21 +463,22 @@ $totalUltra = $currentStats['total_ultra'];
|
||||||
try {
|
try {
|
||||||
const prompt = DinoPrompts.getRandom();
|
const prompt = DinoPrompts.getRandom();
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch('api_proxy.php', {
|
||||||
`https://generativelanguage.googleapis.com/v1beta/models/${apiModels.text}:generateContent?key=${apiKey}`, {
|
method: 'POST',
|
||||||
method: 'POST',
|
headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json'
|
||||||
'Content-Type': 'application/json'
|
},
|
||||||
},
|
body: JSON.stringify({
|
||||||
body: JSON.stringify({
|
apiUrl: `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.text}:generateContent`,
|
||||||
|
payload: {
|
||||||
contents: [{
|
contents: [{
|
||||||
parts: [{
|
parts: [{
|
||||||
text: prompt
|
text: prompt
|
||||||
}]
|
}]
|
||||||
}]
|
}]
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
);
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error("Netzwerkfehler");
|
if (!response.ok) throw new Error("Netzwerkfehler");
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
@ -551,10 +527,6 @@ $totalUltra = $currentStats['total_ultra'];
|
||||||
|
|
||||||
const searchTerm = searchTermInput.value.trim();
|
const searchTerm = searchTermInput.value.trim();
|
||||||
if (!searchTerm) return;
|
if (!searchTerm) return;
|
||||||
if (!apiKey) {
|
|
||||||
showError("API Key fehlt.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof DinoPrompts === 'undefined') {
|
if (typeof DinoPrompts === 'undefined') {
|
||||||
showError("Fehler: prompts.js nicht geladen.");
|
showError("Fehler: prompts.js nicht geladen.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -667,21 +639,22 @@ $totalUltra = $currentStats['total_ultra'];
|
||||||
|
|
||||||
async function fetchDinoFacts(term) {
|
async function fetchDinoFacts(term) {
|
||||||
const factsPrompt = DinoPrompts.getFacts(term);
|
const factsPrompt = DinoPrompts.getFacts(term);
|
||||||
const response = await fetch(
|
const response = await fetch('api_proxy.php', {
|
||||||
`https://generativelanguage.googleapis.com/v1beta/models/${apiModels.text}:generateContent?key=${apiKey}`, {
|
method: 'POST',
|
||||||
method: 'POST',
|
headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json'
|
||||||
'Content-Type': 'application/json'
|
},
|
||||||
},
|
body: JSON.stringify({
|
||||||
body: JSON.stringify({
|
apiUrl: `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.text}:generateContent`,
|
||||||
|
payload: {
|
||||||
contents: [{
|
contents: [{
|
||||||
parts: [{
|
parts: [{
|
||||||
text: factsPrompt
|
text: factsPrompt
|
||||||
}]
|
}]
|
||||||
}]
|
}]
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
);
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errText = await response.text();
|
const errText = await response.text();
|
||||||
if (response.status === 429 || errText.includes('RESOURCE_EXHAUSTED')) {
|
if (response.status === 429 || errText.includes('RESOURCE_EXHAUSTED')) {
|
||||||
|
|
@ -708,12 +681,12 @@ $totalUltra = $currentStats['total_ultra'];
|
||||||
async function fetchDinoImage(dinoData, type, attempt = 1) {
|
async function fetchDinoImage(dinoData, type, attempt = 1) {
|
||||||
const imagePrompt = DinoPrompts.getImage(dinoData);
|
const imagePrompt = DinoPrompts.getImage(dinoData);
|
||||||
|
|
||||||
let url = '';
|
let apiUrl = '';
|
||||||
let payload = {};
|
let payload = {};
|
||||||
let isImagen = false;
|
let isImagen = false;
|
||||||
|
|
||||||
if (type === 'ultra') {
|
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 = {
|
payload = {
|
||||||
contents: [{
|
contents: [{
|
||||||
parts: [{
|
parts: [{
|
||||||
|
|
@ -725,7 +698,7 @@ $totalUltra = $currentStats['total_ultra'];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else if (type === 'pro') {
|
} 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 = {
|
payload = {
|
||||||
instances: [{
|
instances: [{
|
||||||
prompt: imagePrompt
|
prompt: imagePrompt
|
||||||
|
|
@ -736,7 +709,7 @@ $totalUltra = $currentStats['total_ultra'];
|
||||||
};
|
};
|
||||||
isImagen = true;
|
isImagen = true;
|
||||||
} else {
|
} else {
|
||||||
url = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imageEco}:generateContent?key=${apiKey}`;
|
apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${apiModels.imageEco}:generateContent`;
|
||||||
payload = {
|
payload = {
|
||||||
contents: [{
|
contents: [{
|
||||||
parts: [{
|
parts: [{
|
||||||
|
|
@ -750,12 +723,15 @@ $totalUltra = $currentStats['total_ultra'];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch('api_proxy.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify({
|
||||||
|
apiUrl: apiUrl,
|
||||||
|
payload: payload
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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');
|
$userModel = $this->model('User');
|
||||||
$user = $userModel->authenticate($email, $password);
|
$user = $userModel->authenticate($email, $password);
|
||||||
if ($user) {
|
if ($user) {
|
||||||
|
session_regenerate_id(true);
|
||||||
$_SESSION['user_email'] = $email;
|
$_SESSION['user_email'] = $email;
|
||||||
$_SESSION['user_projects'] = $user['projects'] ?? [];
|
$_SESSION['user_projects'] = $user['projects'] ?? [];
|
||||||
$_SESSION['is_admin'] = $user['is_admin'] ?? false;
|
$_SESSION['is_admin'] = $user['is_admin'] ?? false;
|
||||||
|
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||||
header('Location: ' . Config::get('BASE_URL', '/'));
|
header('Location: ' . Config::get('BASE_URL', '/'));
|
||||||
} else {
|
} else {
|
||||||
Flash::set('Access denied: Invalid credentials.');
|
Flash::set('Access denied: Invalid credentials.');
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_set_cookie_params([
|
session_set_cookie_params([
|
||||||
'path' => '/',
|
'path' => '/',
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
|
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
|
||||||
'samesite' => 'Lax'
|
'samesite' => 'Lax'
|
||||||
]);
|
]);
|
||||||
session_start();
|
session_start();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class Controller {
|
class Controller {
|
||||||
|
|
||||||
public function model($model) {
|
protected function model($model) {
|
||||||
$modelPath = __DIR__ . '/../app/models/' . $model . '.php';
|
$modelPath = __DIR__ . '/../app/models/' . $model . '.php';
|
||||||
if (file_exists($modelPath)) {
|
if (file_exists($modelPath)) {
|
||||||
require_once $modelPath;
|
require_once $modelPath;
|
||||||
|
|
@ -11,7 +11,7 @@ class Controller {
|
||||||
throw new Exception("Model '{$model}' existiert nicht.");
|
throw new Exception("Model '{$model}' existiert nicht.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function view($view, $data = []) {
|
protected function view($view, $data = []) {
|
||||||
if (!empty($data)) {
|
if (!empty($data)) {
|
||||||
extract($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);
|
http_response_code($status);
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
echo json_encode($data);
|
echo json_encode($data);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function xml($data) {
|
protected function xml($data) {
|
||||||
header('Content-Type: application/xml; charset=utf-8');
|
header('Content-Type: application/xml; charset=utf-8');
|
||||||
echo $data;
|
echo $data;
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function text($data) {
|
protected function text($data) {
|
||||||
header('Content-Type: text/plain; charset=utf-8');
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
echo $data;
|
echo $data;
|
||||||
exit;
|
exit;
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,9 @@ class Router {
|
||||||
$this->controller = new $this->controller;
|
$this->controller = new $this->controller;
|
||||||
// Method Check
|
// Method Check
|
||||||
if (isset($url[1])) {
|
if (isset($url[1])) {
|
||||||
if (method_exists($this->controller, $url[1])) {
|
$candidateMethod = $url[1];
|
||||||
$this->method = $url[1];
|
if (is_callable([$this->controller, $candidateMethod]) && strpos($candidateMethod, '_') !== 0) {
|
||||||
|
$this->method = $candidateMethod;
|
||||||
unset($url[1]);
|
unset($url[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ class Security {
|
||||||
}
|
}
|
||||||
// CSRF Validation
|
// CSRF Validation
|
||||||
public static function checkCsrf($token) {
|
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.");
|
throw new Exception("Sicherheits-Token ungültig. Bitte lade die Seite neu.");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
error_reporting(E_ALL);
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
if (file_exists(__DIR__ . '/../app/init.php')) {
|
if (file_exists(__DIR__ . '/../app/init.php')) {
|
||||||
require_once __DIR__ . '/../app/init.php';
|
require_once __DIR__ . '/../app/init.php';
|
||||||
} else {
|
} else {
|
||||||
die("Kritischer Fehler: app/init.php wurde nicht gefunden.");
|
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 {
|
try {
|
||||||
// Router
|
// Router
|
||||||
$router = new Router();
|
$router = new Router();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
// 1. Session starten
|
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_set_cookie_params([
|
||||||
|
'path' => '/',
|
||||||
|
'httponly' => true,
|
||||||
|
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
|
||||||
|
'samesite' => 'Lax'
|
||||||
|
]);
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
class Controller {
|
class Controller {
|
||||||
|
|
||||||
// Modell laden
|
// Modell laden
|
||||||
public function model($model) {
|
protected function model($model) {
|
||||||
$modelPath = __DIR__ . '/../app/models/' . $model . '.php';
|
$modelPath = __DIR__ . '/../app/models/' . $model . '.php';
|
||||||
if (file_exists($modelPath)) {
|
if (file_exists($modelPath)) {
|
||||||
require_once $modelPath;
|
require_once $modelPath;
|
||||||
|
|
@ -12,7 +12,7 @@ class Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
// View laden
|
// View laden
|
||||||
public function view($view, $data = []) {
|
protected function view($view, $data = []) {
|
||||||
if (!empty($data)) {
|
if (!empty($data)) {
|
||||||
extract($data);
|
extract($data);
|
||||||
}
|
}
|
||||||
|
|
@ -28,10 +28,22 @@ class Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON-Response für APIs und AJAX
|
// JSON-Response für APIs und AJAX
|
||||||
public function json($data, $status = 200) {
|
protected function json($data, $status = 200) {
|
||||||
http_response_code($status);
|
http_response_code($status);
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
echo json_encode($data);
|
echo json_encode($data);
|
||||||
exit;
|
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())
|
// 2. Methode prüfen (z.B. /user/edit -> Methode edit())
|
||||||
if (isset($url[1])) {
|
if (isset($url[1])) {
|
||||||
if (method_exists($this->controller, $url[1])) {
|
$candidateMethod = $url[1];
|
||||||
$this->method = $url[1];
|
if (is_callable([$this->controller, $candidateMethod]) && strpos($candidateMethod, '_') !== 0) {
|
||||||
|
$this->method = $candidateMethod;
|
||||||
unset($url[1]);
|
unset($url[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ class Security {
|
||||||
|
|
||||||
// Token beim POST-Request prüfen
|
// Token beim POST-Request prüfen
|
||||||
public static function checkCsrf($token) {
|
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.");
|
throw new Exception("Sicherheits-Token ungültig. Bitte lade die Seite neu.");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// Version: 01.10.2025 14:15 (FIX created_at COLUMN)
|
$current_project = 'plants';
|
||||||
|
require_once __DIR__ . '/../auth.php';
|
||||||
|
|
||||||
session_start();
|
session_start();
|
||||||
require_once 'db.php';
|
require_once 'db.php';
|
||||||
require_once __DIR__ . '/vendor/autoload.php';
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
// Version: 24.09.2025 16:29
|
$current_project = 'plants';
|
||||||
|
require_once __DIR__ . '/../auth.php';
|
||||||
|
|
||||||
require_once 'db.php'; // Stellt die $pdo-Verbindung her
|
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