Compare commits

...

10 Commits

Author SHA1 Message Date
c29035c794 asdsad 2025-12-28 01:35:17 +01:00
eff261d28e asa 2025-12-04 02:07:16 +01:00
d50787a1c6 make more flex 2025-12-04 02:06:50 +01:00
e83302c7d8 gitcheck 2025-12-03 00:04:25 +01:00
190dd5724f as 2025-12-03 00:03:28 +01:00
8f45fa04fe asdasd 2025-12-01 02:27:52 +01:00
9ea277c75a sadas 2025-12-01 02:17:48 +01:00
4b533f2d8f up 2025-12-01 01:47:59 +01:00
5ad9c9327a adasd 2025-12-01 01:24:30 +01:00
7eb0994abe asdsad 2025-12-01 01:19:25 +01:00
11 changed files with 238 additions and 121 deletions

View File

@@ -41,6 +41,12 @@ function router_v1_dispatch(array $segments): void
$handler = 'browser_quick_test_handle_request';
break;
// 🔎 NEU: Debug-Endpoint für Session / Cookies
case 'debug.session':
$file = $apibasedir . '/v1/debug/debug.session.php';
$handler = 'debug_session_handle_request';
break;
default:
http_response_code(404);
echo json_encode([

View File

@@ -0,0 +1,22 @@
<?php
// /api/v1/debug/debug.session.php
declare(strict_types=1);
function debug_session_handle_request(): array
{
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
return [
'ok' => true,
'session_id' => session_id(),
'session' => $_SESSION,
'cookies' => $_COOKIE,
'server' => [
'host' => $_SERVER['HTTP_HOST'] ?? null,
'uri' => $_SERVER['REQUEST_URI'] ?? null,
],
];
}

View File

@@ -6,20 +6,25 @@ declare(strict_types=1);
/**
* Handler für: /v1/browser.quick.test
*
* WICHTIG:
* - Keine direkten header()/echo-Ausgaben hier.
* - Der Router (router.v1.php) ruft diese Funktion auf und encodiert das Ergebnis als JSON.
*
* Erwartung des Routers:
* Wird vom Router (/api/router/router.v1.php) aufgerufen.
* Erwartung:
* function browser_quick_test_handle_request(): array
*/
function browser_quick_test_handle_request(): array
{
// Session & DB
// Session sicherstellen (sollte über fileload.php schon aktiv sein,
// aber doppelt ist hier unkritisch)
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
// ---------------------------------------------------------------------
// 0. client_id aus zentraler fileload.php
// ---------------------------------------------------------------------
// fileload.php setzt:
// $GLOBALS['usb_client_id'] UND das Cookie 'usbcheck_client'
$clientId = $GLOBALS['usb_client_id'] ?? ($_COOKIE['usbcheck_client'] ?? null);
// ---------------------------------------------------------------------
// 1. JSON einlesen
// ---------------------------------------------------------------------
@@ -27,8 +32,6 @@ function browser_quick_test_handle_request(): array
$data = json_decode($raw, true);
if (!is_array($data)) {
// Kein gültiges JSON → wir geben nur das Ergebnis zurück,
// HTTP-Status-Code kann der Router setzen, wenn er mag.
return [
'ok' => false,
'error' => 'Invalid JSON payload',
@@ -36,30 +39,21 @@ function browser_quick_test_handle_request(): array
}
// ---------------------------------------------------------------------
// 2. User / Session ermitteln angepasst an dein Login
// 2. User / Session ermitteln (robust, mehrere Varianten)
// ---------------------------------------------------------------------
$userId = null;
$isLoggedIn = 0;
// **Primär**: so wird der User in src/auth/login.php gesetzt:
// $_SESSION['user'] = ['id' => ..., ...]
if (
isset($_SESSION['user']) &&
is_array($_SESSION['user']) &&
!empty($_SESSION['user']['id'])
) {
$userId = (int)$_SESSION['user']['id'];
}
// Fallback A: klassisch
elseif (!empty($_SESSION['user_id'])) {
// A) Klassisch: user_id direkt in der Session
if (!empty($_SESSION['user_id'])) {
$userId = (int)$_SESSION['user_id'];
}
// Fallback B: auth-Block (z.B. $_SESSION['auth']['user_id'])
elseif (
isset($_SESSION['auth']) &&
is_array($_SESSION['auth']) &&
!empty($_SESSION['auth']['user_id'])
) {
// B) Dein aktuelles Login verwendet $_SESSION['user']['id']
elseif (!empty($_SESSION['user']) && is_array($_SESSION['user']) && !empty($_SESSION['user']['id'])) {
$userId = (int)$_SESSION['user']['id'];
}
// C) Optionaler auth-Block (z.B. $_SESSION['auth']['user_id'])
elseif (!empty($_SESSION['auth']) && is_array($_SESSION['auth']) && !empty($_SESSION['auth']['user_id'])) {
$userId = (int)$_SESSION['auth']['user_id'];
}
@@ -78,13 +72,13 @@ function browser_quick_test_handle_request(): array
$modeRequested = $data['mode_requested'] ?? 'unknown';
$meta = $data['meta'] ?? [];
// Hier könntest du später Browser/OS parsen
// Browser/OS vorerst leer, später per Parser füllen
$browserName = null;
$browserVersion = null;
$osName = null;
$osVersion = null;
// Beispiel: Gesamtmenge geschriebener/verifizierter Bytes aggregieren
// Gesamtmenge geschriebener/verifizierter Bytes aggregieren
$measuredBytes = 0;
if (!empty($data['quick']) && is_array($data['quick'])) {
@@ -100,8 +94,7 @@ function browser_quick_test_handle_request(): array
// Kapazitätsstatus vorerst neutral
$capacityStatus = 'unknown';
// Volume-/Stick-Daten hast du aktuell im Browser noch nicht separat,
// darum bleiben diese Felder (erstmal) NULL:
// Volume-/Stick-Daten aktuell noch nicht separat ermittelt
$volumeLabel = null;
$manufacturer = null;
$modelName = null;
@@ -140,7 +133,8 @@ function browser_quick_test_handle_request(): array
filesystem,
test_report_json,
ip_address,
session_id
session_id,
client_id
)
VALUES (
:user_id,
@@ -160,7 +154,8 @@ function browser_quick_test_handle_request(): array
:filesystem,
:test_report_json,
:ip_address,
:session_id
:session_id,
:client_id
)
";
@@ -185,29 +180,32 @@ function browser_quick_test_handle_request(): array
'test_report_json' => $testReportJson,
'ip_address' => $ipAddress,
'session_id' => $sessionId,
'client_id' => $clientId,
]);
$id = (int)$pdo->lastInsertId();
// Optionales Debug-Logging, falls trotz Login kein user_id ankam
if ($isLoggedIn === 0 && !empty($_SESSION['user'])) {
error_log('[usbcheck] browser.quick.test: SESSION[user] vorhanden, aber user_id blieb leer: ' . json_encode($_SESSION['user']));
}
// DEBUG-Ausgabe später für PROD aufräumen
return [
'ok' => true,
'id' => $id,
'mode' => $modeRequested,
'measured_bytes' => $measuredBytes ?: null,
'debug_user_id' => $userId,
'debug_is_logged_in' => $isLoggedIn,
'debug_session_id' => $sessionId,
'debug_client_id' => $clientId,
'debug_session_has_user' => isset($_SESSION['user']),
'debug_session_user' => $_SESSION['user'] ?? null,
];
} catch (Throwable $e) {
// Fehler ins Error-Log schreiben (nur Server-seitig sichtbar)
error_log('[usbcheck] web_quicktests insert failed: ' . $e->getMessage());
return [
'ok' => false,
'error' => 'DB error',
'debug' => $e->getMessage(), // zum Debuggen ggf. später entfernen
'debug' => $e->getMessage(),
];
}
}

4
config/domaindata.php Normal file
View File

@@ -0,0 +1,4 @@
<?php
define('APP_DOMAIN_NAME', 'usbcheck.it');
define('APP_PREFIX', 'usbcheck');

View File

@@ -1,40 +1,65 @@
<?php
// 0) Umgebung / Domains / Error-Level
// -----------------------------------------------------------
// 0) Umgebung / Domains / Error-Level laden
// → Diese Datei DEFINIERT die Konstanten wie
// APP_COOKIE_PREFIX, APP_COOKIE_DOMAIN, APP_ENV etc.
// -----------------------------------------------------------
require_once __DIR__ . "/config.php";
// ----------------------------------------------------------
// Session starten (gemeinsam für Frontend + API Subdomains)
// Diese Werte später ins Template schieben:
$GLOBALS['app_env'] = APP_ENV;
$GLOBALS['app_base_url'] = APP_URL_PRIMARY;
$GLOBALS['app_api_base'] = $apiBaseUrl;
// -----------------------------------------------------------
// set cookie / session parameters
// -----------------------------------------------------------
if (!defined('CUSTOM_PREFIX')) {
define('CUSTOM_PREFIX', APP_PREFIX);
}
if(!defined('APP_COOKIE_PREFIX')) {
if(APP_ENV==="staging"){
define('APP_COOKIE_PREFIX', APP_PREFIX.'_stg'.'_');
} else
{
define('APP_COOKIE_PREFIX', APP_PREFIX.'_');
}
}
if (!defined('APP_COOKIE_DOMAIN')) {
// Fallback: aktuelle Domain des Hosts
define('APP_COOKIE_DOMAIN', '.'.APP_DOMAIN_PRIMARY);
define('APP_PRIMARY_DOMAIN', APP_DOMAIN_PRIMARY);
}
if (!defined('APP_CLIENT_COOKIE_LIFETIME')) {
define('APP_CLIENT_COOKIE_LIFETIME', 365 * 24 * 60 * 60); // 1 Jahr
}
// Einheitliche Cookie-Namen (projektübergreifend steuerbar)
$sessionCookieName = APP_COOKIE_PREFIX . 'session';
$clientCookieName = APP_COOKIE_PREFIX . 'client';
// -----------------------------------------------------------
// 1) PHP-Session starten
// -----------------------------------------------------------
if (php_sapi_name() !== 'cli') {
if (session_status() === PHP_SESSION_NONE) {
// Einheitlicher Name für alle Teilbereiche
session_name('usbcheck_session');
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
// Cookie-Domain so wählen, dass ALLE Subdomains von usbcheck.it
// dieselbe Session sehen (staging., api.staging., www., …)
$cookieDomain = '';
if (!empty($_SERVER['HTTP_HOST'])) {
$host = $_SERVER['HTTP_HOST'];
// alles was auf "usbcheck.it" endet, bekommt die gemeinsame Domain
if (substr($host, -strlen('usbcheck.it')) === 'usbcheck.it') {
// wirkt für usbcheck.it, staging.usbcheck.it, api.staging.usbcheck.it, ...
$cookieDomain = '.usbcheck.it';
}
}
session_name($sessionCookieName);
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
// WICHTIG: hier die Domain, damit API + Frontend teilen
'domain' => $cookieDomain ?: '',
'secure' => $isHttps,
'domain' => APP_COOKIE_DOMAIN ?: '',
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
'httponly' => true,
// eTLD+1 ist gleich (usbcheck.it), daher reicht Lax für Same-Site
'samesite' => 'Lax',
]);
@@ -42,17 +67,55 @@ if (php_sapi_name() !== 'cli') {
}
}
require_once __DIR__ . '/i18n.php'; // <— zentrale Sprachlogik
// ab hier kannst du überall $GLOBALS['lang'] und $GLOBALS['availableLangs'] nutzen
// und für JS:
$usbConfig = [
// ... dein sonstiges Zeug ...
'i18n' => app_i18n_get_frontend_config(),
];
// -----------------------------------------------------------
// 7) Rest des Systems laden
// 2) Persistente Client-ID (für Tracking über Besuche hinweg)
// -----------------------------------------------------------
if (php_sapi_name() !== 'cli') {
$clientId = $_COOKIE[$clientCookieName] ?? null;
// Erwartet wird: 64 Hex-Zeichen (32 Bytes)
if (
!is_string($clientId) ||
$clientId === '' ||
!preg_match('/^[a-f0-9]{64}$/', $clientId)
) {
// neue ID erzeugen
try {
$clientId = bin2hex(random_bytes(32)); // 32 bytes → 64 hex
} catch (Throwable $e) {
$clientId = bin2hex(openssl_random_pseudo_bytes(32));
}
$cookieOpts = [
'expires' => time() + APP_CLIENT_COOKIE_LIFETIME,
'path' => '/',
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
'httponly' => false, // JS darf es lesen, wenn erwünscht
'samesite' => 'Lax',
];
if (!empty(APP_COOKIE_DOMAIN)) {
$cookieOpts['domain'] = APP_COOKIE_DOMAIN;
}
setcookie($clientCookieName, $clientId, $cookieOpts);
$_COOKIE[$clientCookieName] = $clientId;
}
// global verfügbar machen (NEUER NAME!)
$GLOBALS['cookie_client_id'] = $clientId;
}
// -----------------------------------------------------------
// 3) Sprachlogik laden (bleibt sinnvoll zentral)
// -----------------------------------------------------------
require_once __DIR__ . '/i18n.php';
// -----------------------------------------------------------
// 4) Rest des Systems laden (DB, Funktionen, Hilfs-Libs)
// -----------------------------------------------------------
require_once __DIR__ . "/db.php";
require_once __DIR__ . '/../src/functions.php';

View File

@@ -3,13 +3,16 @@ ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . "/domaindata.php";
// Umgebung (optional, aber hilfreich für Debugging / Logik)
define('APP_ENV', 'prod'); // oder 'prod', 'local', ...
if (!defined('ASSET_VERSION')) {
define('ASSET_VERSION', '2024-11-22'); // oder deine aktuelle Version
}
// Domain-Konfiguration (kann pro Umgebung angepasst werden)
if (!defined('APP_DOMAIN_PRIMARY')) {
define('APP_DOMAIN_PRIMARY', 'usbcheck.it');
define('APP_DOMAIN_PRIMARY', APP_DOMAIN_NAME);
}
if (!defined('APP_URL_PRIMARY')) {
define('APP_URL_PRIMARY', 'https://' . APP_DOMAIN_PRIMARY);
@@ -26,10 +29,6 @@ define('MATOMO_URL', 'https://matomo.my-statistics.info/');
define('MATOMO_ENABLED', true);
define('MATOMO_SITE_ID', 7);
$env = 'prod';
$baseUrl = 'https://usbcheck.it';
$apiBaseUrl = 'https://api.usbcheck.it';
$baseUrl = 'https://'.APP_DOMAIN_NAME;
$apiBaseUrl = 'https://api.'.APP_DOMAIN_NAME;
// Diese Werte später ins Template schieben:
$GLOBALS['usb_env'] = $env;
$GLOBALS['usb_base_url'] = $baseUrl;
$GLOBALS['usb_api_base'] = $apiBaseUrl;

View File

@@ -3,13 +3,19 @@ ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . "/domaindata.php";
// Umgebung (optional, aber hilfreich für Debugging / Logik)
define('APP_ENV', 'staging'); // oder 'prod', 'local', ...
if (!defined('ASSET_VERSION')) {
define('ASSET_VERSION', time()); // oder deine aktuelle Version
}
// Domain-Konfiguration (kann pro Umgebung angepasst werden)
if (!defined('APP_DOMAIN_PRIMARY')) {
define('APP_DOMAIN_PRIMARY', 'staging.usbcheck.it');
define('APP_DOMAIN_PRIMARY', 'staging.'.APP_DOMAIN_NAME);
}
if (!defined('APP_URL_PRIMARY')) {
define('APP_URL_PRIMARY', 'https://' . APP_DOMAIN_PRIMARY);
@@ -23,12 +29,8 @@ if (!defined('APP_URL_FAKECHECK')) {
// Matomo Einstellungen
define('MATOMO_URL', 'https://matomo.my-statistics.info/');
define('MATOMO_ENABLED', false);
define('MATOMO_ENABLED', true);
define('MATOMO_SITE_ID', 8);
$env = 'staging';
$baseUrl = 'https://'.APP_DOMAIN_PRIMARY;
$apiBaseUrl = 'https://api.'.APP_DOMAIN_PRIMARY;
// Diese Werte später ins Template schieben:
$GLOBALS['usb_env'] = $env;
$GLOBALS['usb_base_url'] = APP_URL_PRIMARY;
$GLOBALS['usb_api_base'] = $apiBaseUrl;

View File

@@ -3,4 +3,6 @@ in der tpl-Funktion bauen wir die Pfade fix, aber aufgrund nachfolgender Struktu
tpl([dateiname],[type = structure, landing],[(optional)site=main, fakecheck)
Weil dann kann ich im Hintergrund die Pfade ändern und muss das nur in der tpl-Funktion anpassen. Die Seiten bleiben wie sie sind.
Weil dann kann ich im Hintergrund die Pfade ändern und muss das nur in der tpl-Funktion anpassen. Die Seiten bleiben wie sie sind.
next

View File

@@ -3,56 +3,73 @@
// Host-Infos ermitteln (für URLs / Redirects etc.)
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? app_primary_domain();
$host = $_SERVER['HTTP_HOST'] ?? (function_exists('app_primary_domain') ? app_primary_domain() : 'localhost');
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
// Aktuelle Sprache & Sprachen aus den GLOBALS (fileload.php)
$currentLang = $GLOBALS['lang'] ?? ($lang ?? 'en');
$allAvailable = $GLOBALS['availableLangs'] ?? [];
// Aktuelle Sprache & Sprachen aus den GLOBALS (kommen aus fileload.php / i18n.php)
$currentLang = $GLOBALS['lang'] ?? ($lang ?? 'en');
$allAvailable = $GLOBALS['availableLangs'] ?? [];
// Optional: Environment aus config.php (du hattest $env → $GLOBALS['usb_env'])
$env = $GLOBALS['usb_env'] ?? 'prod';
// Environment & Basis-URLs aus config.php / fileload.php
$appEnv = $GLOBALS['app_env'] ?? (defined('APP_ENV') ? APP_ENV : 'prod');
$appBaseUrl = $GLOBALS['app_base_url'] ?? ($scheme . '://' . $host);
$appApiBase = $GLOBALS['app_api_base'] ?? ($scheme . '://api.' . ($host));
// Optional: Helfer-Funktionen nutzen, falls vorhanden
$primaryDomain = function_exists('app_primary_domain') ? app_primary_domain() : $host;
$primaryUrl = function_exists('app_primary_url') ? app_primary_url() : ($scheme . '://' . $primaryDomain);
$fakecheckDomain = function_exists('app_fakecheck_domain') ? app_fakecheck_domain() : null;
$fakecheckUrl = function_exists('app_fakecheck_url') ? app_fakecheck_url() : null;
// i18n-Config aus deiner zentralen i18n-Logik
$i18nConfig = function_exists('app_i18n_get_frontend_config')
? app_i18n_get_frontend_config()
: [
'available' => $allAvailable,
'current' => $currentLang,
];
// -----------------------------------------------
// USBCheck JavaScript-Konfiguration
// -----------------------------------------------
$usbConfig = [
'lang' => $currentLang,
'lang' => $currentLang,
// Basis-Pfade
'assetsBase' => '/assets',
'assetsBase' => '/assets',
// Versionierung für JS/CSS
'assetVersion'=> defined('ASSET_VERSION') ? ASSET_VERSION : null,
'assetVersion' => defined('ASSET_VERSION') ? ASSET_VERSION : null,
// Environment (prod, staging, dev)
'env' => $env,
'env' => $appEnv,
// Domains
'domains' => [
'primaryDomain' => app_primary_domain(),
'primaryUrl' => app_primary_url(),
'fakecheckDomain' => app_fakecheck_domain(),
'fakecheckUrl' => app_fakecheck_url(),
'domains' => [
'primaryDomain' => $primaryDomain,
'primaryUrl' => $primaryUrl,
'fakecheckDomain' => $fakecheckDomain,
'fakecheckUrl' => $fakecheckUrl,
],
// Fakecheck-Tool-Config
'fakecheck' => [
'baseUrl' => $GLOBALS['usb_base_url'] ?? '',
'apiBaseUrl' => $GLOBALS['usb_api_base'] ?? 'https://api.usbcheck.it',
'fakecheck' => [
'baseUrl' => $appBaseUrl,
'apiBaseUrl' => $appApiBase,
'locale' => $currentLang,
],
// i18n-Konfiguration
'i18n' => [
'available' => $allAvailable,
'current' => $currentLang,
],
// i18n-Konfiguration (zentrale Struktur)
'i18n' => $i18nConfig,
];
?>
<script>
window.usbConfig = <?= json_encode(
// Zentrales JS-Config-Objekt für deine Tools
window.usbcheck = <?= json_encode(
$usbConfig,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE
) ?>;
// Alias für eventuelle ältere Stellen
window.usbConfig = window.usbcheck;
</script>

View File

@@ -20,3 +20,5 @@ tpl('security', 'landing', 'main');
tpl('faq', 'landing', 'main');
tpl('layout_end'); // structure/footer.php

View File

@@ -38,7 +38,6 @@ CREATE TABLE IF NOT EXISTS users (
-- ============================================================
-- USB DEVICES vom Nutzer gespeicherte USB-Sticks
-- Ein Benutzer kann mehrere Sticks speichern.
-- ============================================================
CREATE TABLE IF NOT EXISTS usb_devices (
@@ -67,7 +66,6 @@ CREATE TABLE IF NOT EXISTS usb_devices (
-- ============================================================
-- USB TEST RESULTS Schnelltest + Pro-Test
-- Jedes Testergebnis gehört zu einem Stick.
-- ============================================================
CREATE TABLE IF NOT EXISTS usb_tests (
@@ -97,7 +95,7 @@ CREATE TABLE IF NOT EXISTS usb_tests (
-- Metadaten
test_report_json JSON NULL,
ip_address VARCHAR(45) NULL, -- ipv6 kompatibel
ip_address VARCHAR(45) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
@@ -113,7 +111,6 @@ CREATE TABLE IF NOT EXISTS usb_tests (
-- ============================================================
-- WEB QUICKTESTS Browser-Schnellcheck (Gast + eingeloggt)
-- Ergebnisse können anonym oder usergebunden sein.
-- ============================================================
CREATE TABLE IF NOT EXISTS web_quicktests (
@@ -132,13 +129,13 @@ CREATE TABLE IF NOT EXISTS web_quicktests (
os_name VARCHAR(100) NULL,
os_version VARCHAR(50) NULL,
-- Stick-Infos aus dem Schnelldurchlauf (soweit ermittelbar)
volume_label VARCHAR(255) NULL, -- z.B. "USB DISK", "NO NAME"
-- Stick-Infos
volume_label VARCHAR(255) NULL,
manufacturer VARCHAR(255) NULL,
model_name VARCHAR(255) NULL,
usb_type ENUM('USB 2.0', 'USB 3.0', 'USB 3.1', 'USB 3.2', 'USB 4.0') NULL,
-- Kapazitätsdaten (Hersteller vs. gemessen im Schnellcheck)
-- Kapazität
advertised_capacity_bytes BIGINT UNSIGNED NULL,
measured_capacity_bytes BIGINT UNSIGNED NULL,
@@ -147,13 +144,16 @@ CREATE TABLE IF NOT EXISTS web_quicktests (
filesystem VARCHAR(64) NULL,
-- Detaildaten (z.B. Block-Infos, Logs, Messpunkte)
-- Vollständiger Testreport
test_report_json JSON NULL,
-- Meta
ip_address VARCHAR(45) NULL,
session_id VARCHAR(64) NULL,
-- NEU: persistente Browser-/Client-ID zur Wiedererkennung
client_id VARCHAR(64) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
@@ -162,5 +162,7 @@ CREATE TABLE IF NOT EXISTS web_quicktests (
ON DELETE CASCADE,
FOREIGN KEY (usb_device_id) REFERENCES usb_devices(id)
ON DELETE CASCADE
ON DELETE CASCADE,
INDEX idx_web_quicktests_client_id (client_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;