M2 Umsetzung
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
.env.local
|
.env.local
|
||||||
.local/
|
.local/
|
||||||
uploads/
|
uploads/
|
||||||
|
var/
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
if (!defined('APP_ROOT')) {
|
||||||
|
define('APP_ROOT', dirname(__DIR__));
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_env(string $name, ?string $default = null): ?string
|
||||||
|
{
|
||||||
|
$value = getenv($name);
|
||||||
|
if ($value === false || $value === '') {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_is_dev(): bool
|
||||||
|
{
|
||||||
|
return app_env('APP_ENV', 'prod') === 'dev';
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_is_https(): bool
|
||||||
|
{
|
||||||
|
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
||||||
|
|| (($_SERVER['SERVER_PORT'] ?? null) === '443');
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_start_session(): void
|
||||||
|
{
|
||||||
|
if (PHP_SAPI === 'cli' || session_status() === PHP_SESSION_ACTIVE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionPath = app_env('APP_SESSION_PATH', APP_ROOT . '/var/sessions');
|
||||||
|
if ($sessionPath !== null && !is_dir($sessionPath)) {
|
||||||
|
@mkdir($sessionPath, 0700, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sessionPath !== null && is_dir($sessionPath) && is_writable($sessionPath)) {
|
||||||
|
session_save_path($sessionPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
session_set_cookie_params([
|
||||||
|
'lifetime' => 0,
|
||||||
|
'path' => '/',
|
||||||
|
'domain' => '',
|
||||||
|
'secure' => app_is_https(),
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
]);
|
||||||
|
|
||||||
|
session_name(app_env('APP_SESSION_NAME', 'kaffeeliste_session') ?? 'kaffeeliste_session');
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_csrf_token(): string
|
||||||
|
{
|
||||||
|
app_start_session();
|
||||||
|
|
||||||
|
if (!isset($_SESSION) || !is_array($_SESSION)) {
|
||||||
|
$_SESSION = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $_SESSION['csrf_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_csrf_field(): string
|
||||||
|
{
|
||||||
|
return '<input type="hidden" name="csrf_token" value="'
|
||||||
|
. htmlspecialchars(app_csrf_token(), ENT_QUOTES, 'UTF-8')
|
||||||
|
. '">';
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_verify_csrf(?string $token): bool
|
||||||
|
{
|
||||||
|
app_start_session();
|
||||||
|
|
||||||
|
if (!isset($_SESSION) || !is_array($_SESSION)) {
|
||||||
|
$_SESSION = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_string($token)
|
||||||
|
&& isset($_SESSION['csrf_token'])
|
||||||
|
&& is_string($_SESSION['csrf_token'])
|
||||||
|
&& hash_equals($_SESSION['csrf_token'], $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_require_csrf(): void
|
||||||
|
{
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!app_verify_csrf($_POST['csrf_token'] ?? null)) {
|
||||||
|
http_response_code(419);
|
||||||
|
die('CSRF validation failed.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/app/bootstrap.php';
|
||||||
|
|
||||||
|
app_start_session();
|
||||||
|
|
||||||
if ((getenv('APP_DB_DRIVER') ?: '') === 'mysql' && !function_exists('sqlsrv_connect')) {
|
if ((getenv('APP_DB_DRIVER') ?: '') === 'mysql' && !function_exists('sqlsrv_connect')) {
|
||||||
require_once __DIR__ . '/lib/sqlsrv_mysql_compat.php';
|
require_once __DIR__ . '/lib/sqlsrv_mysql_compat.php';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS kl_Mitarbeiter (
|
||||||
|
MitarbeiterID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
Name VARCHAR(255) NOT NULL,
|
||||||
|
Email VARCHAR(255) NOT NULL,
|
||||||
|
paypalname VARCHAR(255) NULL,
|
||||||
|
aktiv TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
admin TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
UNIQUE KEY uq_kl_mitarbeiter_email (Email)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS kl_config (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
KostenproStrich DECIMAL(10,2) NOT NULL DEFAULT 0.20,
|
||||||
|
paypaluse TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
paypallink VARCHAR(1000) NOT NULL DEFAULT '',
|
||||||
|
strichperweb TINYINT(1) NOT NULL DEFAULT 1
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS kl_Einzahlungen (
|
||||||
|
EinzahlungsID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
MitarbeiterID INT NOT NULL,
|
||||||
|
Betrag DECIMAL(10,2) NOT NULL,
|
||||||
|
Datum DATETIME NOT NULL,
|
||||||
|
KEY idx_kl_einzahlungen_mitarbeiter_datum (MitarbeiterID, Datum),
|
||||||
|
CONSTRAINT fk_kl_einzahlungen_mitarbeiter
|
||||||
|
FOREIGN KEY (MitarbeiterID) REFERENCES kl_Mitarbeiter(MitarbeiterID)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS kl_Kaffeeverbrauch (
|
||||||
|
VerbrauchID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
MitarbeiterID INT NOT NULL,
|
||||||
|
AnzahlStriche INT NOT NULL,
|
||||||
|
Kosten DECIMAL(10,2) NOT NULL,
|
||||||
|
KostenproStrich DECIMAL(10,2) NOT NULL,
|
||||||
|
Datum DATETIME NOT NULL,
|
||||||
|
Eintragsart INT NULL,
|
||||||
|
KEY idx_kl_verbrauch_mitarbeiter_datum (MitarbeiterID, Datum),
|
||||||
|
CONSTRAINT fk_kl_verbrauch_mitarbeiter
|
||||||
|
FOREIGN KEY (MitarbeiterID) REFERENCES kl_Mitarbeiter(MitarbeiterID)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS kl_hinweise (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nachricht TEXT NOT NULL,
|
||||||
|
gueltig_bis DATETIME NOT NULL,
|
||||||
|
KEY idx_kl_hinweise_gueltig_bis (gueltig_bis)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS CoffeeSurveyVotedEmails (
|
||||||
|
EmailNorm VARCHAR(255) PRIMARY KEY,
|
||||||
|
CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS CoffeeSurveyResponses (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
Q1_ListEase INT NOT NULL,
|
||||||
|
Q2_ListSatisfaction INT NOT NULL,
|
||||||
|
Q3_CoffeeQuality INT NOT NULL,
|
||||||
|
Q4_WebsiteSatisfaction INT NOT NULL,
|
||||||
|
Q5_Drinks TEXT NOT NULL,
|
||||||
|
Q5_DrinksOther TEXT NULL,
|
||||||
|
Q6_NewVarieties TEXT NULL,
|
||||||
|
Q7_ListProblem VARCHAR(100) NOT NULL,
|
||||||
|
Q7_ListProblemOther TEXT NULL,
|
||||||
|
Q8_Improvements TEXT NOT NULL,
|
||||||
|
Q8_ImprovementsOther TEXT NULL,
|
||||||
|
Q9_BetterIdeas TEXT NULL,
|
||||||
|
CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dev_baseline_metadata (
|
||||||
|
meta_key VARCHAR(100) PRIMARY KEY,
|
||||||
|
meta_value TEXT NOT NULL,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT INTO kl_config (id, KostenproStrich, paypaluse, paypallink, strichperweb)
|
||||||
|
VALUES (1, 0.20, 0, '', 1)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
KostenproStrich = VALUES(KostenproStrich),
|
||||||
|
paypaluse = VALUES(paypaluse),
|
||||||
|
strichperweb = VALUES(strichperweb);
|
||||||
|
|
||||||
|
INSERT INTO dev_baseline_metadata (meta_key, meta_value)
|
||||||
|
VALUES
|
||||||
|
('baseline_kind', 'reconstructed_mysql_dev'),
|
||||||
|
('legacy_database_status', 'no_ms_sql_legacy_snapshot_available'),
|
||||||
|
('legacy_behavior_source', 'code_and_screenshots'),
|
||||||
|
('visual_reference_path', 'docs/m0/screenshots'),
|
||||||
|
('schema_source', 'database/migrations/0001_legacy_mysql_baseline.sql')
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
meta_value = VALUES(meta_value);
|
||||||
+15
-2
@@ -24,10 +24,20 @@ export DB_USER="example_user"
|
|||||||
export DB_PASS="example_password"
|
export DB_PASS="example_password"
|
||||||
export DEV_AUTH_EMAIL="admin@test.local"
|
export DEV_AUTH_EMAIL="admin@test.local"
|
||||||
export DEV_AUTH_NAME="Test Admin"
|
export DEV_AUTH_NAME="Test Admin"
|
||||||
|
# Optional: Standard ist var/sessions im Repository.
|
||||||
|
export APP_SESSION_PATH="/path/to/writable/sessions"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testdatenbank initialisieren
|
## Testdatenbank initialisieren
|
||||||
|
|
||||||
|
Migrationen anwenden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php scripts/migrate.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Dev-Admin und Basistestdaten anlegen:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php scripts/init-mysql-dev.php
|
php scripts/init-mysql-dev.php
|
||||||
```
|
```
|
||||||
@@ -53,6 +63,9 @@ der Port ueber die Ports-Ansicht weitergeleitet werden.
|
|||||||
## Hinweise
|
## Hinweise
|
||||||
|
|
||||||
- Der Dev-Login umgeht AD/LDAP und nutzt `DEV_AUTH_EMAIL`.
|
- Der Dev-Login umgeht AD/LDAP und nutzt `DEV_AUTH_EMAIL`.
|
||||||
- Die Tabellen werden aus `database/mysql-dev-schema.sql` angelegt.
|
- Die Tabellen werden ueber `database/migrations/` angelegt.
|
||||||
|
- `database/mysql-dev-schema.sql` bleibt als historische Dev-Schema-Baseline
|
||||||
|
erhalten; der aktive Weg ist `scripts/migrate.php`.
|
||||||
|
- Session-Dateien liegen standardmaessig unter `var/sessions`; `var/` wird von
|
||||||
|
Git ignoriert.
|
||||||
- Der Dev-Modus ist nur fuer lokale Tests gedacht, nicht fuer Produktion.
|
- Der Dev-Modus ist nur fuer lokale Tests gedacht, nicht fuer Produktion.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# M2 Technisches Fundament
|
||||||
|
|
||||||
|
Stand: 2026-07-12
|
||||||
|
|
||||||
|
M2 fuehrt die technischen Grundbausteine fuer den SaaS-Umbau ein, ohne das
|
||||||
|
Legacy-Verhalten oder das bestehende Design zu veraendern.
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
- Versionierte Datenbankmigrationen statt loser Schema-Ausfuehrung.
|
||||||
|
- Zentrales Bootstrap fuer Umgebung, Session und CSRF-Helper.
|
||||||
|
- Bestehende Legacy-Seiten bleiben kompatibel.
|
||||||
|
- Layout-Trennung wird vorbereitet, aber noch nicht in bestehende Seiten
|
||||||
|
hineingezogen.
|
||||||
|
|
||||||
|
## Umgesetzte Bausteine
|
||||||
|
|
||||||
|
### Bootstrap
|
||||||
|
|
||||||
|
- `app/bootstrap.php` definiert zentrale Helfer:
|
||||||
|
- `app_env()`
|
||||||
|
- `app_is_dev()`
|
||||||
|
- `app_start_session()`
|
||||||
|
- `app_csrf_token()`
|
||||||
|
- `app_csrf_field()`
|
||||||
|
- `app_verify_csrf()`
|
||||||
|
- `app_require_csrf()`
|
||||||
|
- `config.php` laedt den Bootstrap und startet die Session mit sicheren
|
||||||
|
Cookie-Optionen.
|
||||||
|
- Sessions werden standardmaessig unter `var/sessions` abgelegt, weil die
|
||||||
|
lokale PHP-Umgebung keinen beschreibbaren Systempfad garantiert. `var/` ist
|
||||||
|
von Git ignoriert. Der Pfad kann ueber `APP_SESSION_PATH` ueberschrieben
|
||||||
|
werden.
|
||||||
|
- CSRF ist bewusst noch nicht global erzwungen. Die vorhandenen POST-Seiten
|
||||||
|
werden spaeter einzeln umgestellt, damit keine Formulare oder Spezialflows
|
||||||
|
brechen.
|
||||||
|
|
||||||
|
### Migrationen
|
||||||
|
|
||||||
|
- `database/migrations/0001_legacy_mysql_baseline.sql` bildet die bisherige
|
||||||
|
MySQL-Dev-Baseline als erste versionierte Migration ab.
|
||||||
|
- `scripts/dev-db.php` verwaltet `schema_migrations` und fuehrt neue
|
||||||
|
Migrationen idempotent aus.
|
||||||
|
- `scripts/migrate.php` ist der direkte Runner fuer Migrationen.
|
||||||
|
- `scripts/init-mysql-dev.php` nutzt ab jetzt ebenfalls die Migrationslogik.
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
- `header.php` und `footer.php` bleiben vorerst Legacy-Wrapper.
|
||||||
|
- Die spaetere Trennung in Public-Layout und App-Layout wird erst umgesetzt,
|
||||||
|
wenn die neue Route-/View-Struktur steht.
|
||||||
|
- Die bestehende HTML5-UP-Struktur, Sidebar und Assets bleiben unveraendert.
|
||||||
|
|
||||||
|
## Ausfuehrung
|
||||||
|
|
||||||
|
Mit normaler PHP-CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php scripts/migrate.php
|
||||||
|
php scripts/init-mysql-dev.php
|
||||||
|
php scripts/check-golden-master.php
|
||||||
|
php scripts/http-smoke.php
|
||||||
|
```
|
||||||
|
|
||||||
|
In der aktuellen lokalen Umgebung:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LD_LIBRARY_PATH="$PWD/.local/php/usr/lib/x86_64-linux-gnu:$PWD/.local/php/usr/lib/x86_64-linux-gnu/sasl2" \
|
||||||
|
"$PWD/.local/php/usr/bin/php8.3" \
|
||||||
|
-c "$PWD/.local/php-dev.ini" \
|
||||||
|
scripts/migrate.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Skripte erwarten die bekannten Dev-Umgebungsvariablen `DB_HOST`, `DB_NAME`,
|
||||||
|
`DB_USER` und `DB_PASS`. `scripts/init-mysql-dev.php` braucht zusaetzlich
|
||||||
|
`DEV_AUTH_EMAIL`.
|
||||||
|
|
||||||
|
## Aktueller Pruefstatus
|
||||||
|
|
||||||
|
- Migration `0001_legacy_mysql_baseline.sql` erfolgreich angewendet.
|
||||||
|
- Zweiter Migrationslauf meldet: Datenbank ist aktuell.
|
||||||
|
- PHP-Syntax fuer Bootstrap, Migrationen, Init-Skript und Config ist sauber.
|
||||||
|
- Session-Start laeuft in der lokalen Dev-Umgebung ohne PHP-Warnings ueber
|
||||||
|
`var/sessions`.
|
||||||
|
- Golden Master weiterhin gruen mit 104 Assertions.
|
||||||
|
- HTTP-Smoke weiterhin gruen mit 14 sicheren Seiten.
|
||||||
|
|
||||||
|
## Bewusste Grenzen
|
||||||
|
|
||||||
|
- Keine Tenant-/User-/Rollen-Tabellen in M2. Diese gehoeren zu M3.
|
||||||
|
- Keine globale CSRF-Erzwingung in M2. Die Absicherung der POST-Seiten erfolgt
|
||||||
|
schrittweise.
|
||||||
|
- Kein Layout-Umbau in M2. Die visuelle Struktur bleibt stabil.
|
||||||
|
- PDF-/Mail-/Jahresprozesse bleiben als M6-Themen offen.
|
||||||
|
|
||||||
|
## Naechste Schritte
|
||||||
|
|
||||||
|
1. POST-Seiten einzeln mit `app_csrf_field()` und `app_require_csrf()`
|
||||||
|
absichern.
|
||||||
|
2. Eine duenne View-/Layout-Struktur vorbereiten, ohne Header/Footer-Markup
|
||||||
|
sofort zu verschieben.
|
||||||
|
3. Danach M3 starten: Tenants, User, Registrierung, Login und Rollen.
|
||||||
@@ -282,7 +282,7 @@ Uebersicht:
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| M0 | Baseline | Bestand, Sicherheit und Designreferenz sind dokumentiert |
|
| M0 | Baseline | Bestand, Sicherheit und Designreferenz sind dokumentiert |
|
||||||
| M1 | Golden Master | Legacy-Ergebnisse sind als Vergleichsbasis eingefroren; HTTP-Smoke prueft sichere Seiten |
|
| M1 | Golden Master | Legacy-Ergebnisse sind als Vergleichsbasis eingefroren; HTTP-Smoke prueft sichere Seiten |
|
||||||
| M2 | Technisches Fundament | Public-/App-Struktur, Bootstrap, Session und CSRF stehen |
|
| M2 | Technisches Fundament | Migrationen, Bootstrap, Session und CSRF-Helper stehen |
|
||||||
| M3 | SaaS-Basis | Tenants, User, Registrierung, Login und Rollen funktionieren |
|
| M3 | SaaS-Basis | Tenants, User, Registrierung, Login und Rollen funktionieren |
|
||||||
| M4 | Datenmigration | Legacy-Daten sind tenant-sicher im Zielmodell abgebildet |
|
| M4 | Datenmigration | Legacy-Daten sind tenant-sicher im Zielmodell abgebildet |
|
||||||
| M5 | App-Kern | Dashboard, Striche, Einzahlungen, Mitglieder und Liste laufen |
|
| M5 | App-Kern | Dashboard, Striche, Einzahlungen, Mitglieder und Liste laufen |
|
||||||
@@ -373,19 +373,27 @@ Schritte:
|
|||||||
- Projektstruktur festlegen: Public-Routes, App-Routes, Views/Templates,
|
- Projektstruktur festlegen: Public-Routes, App-Routes, Views/Templates,
|
||||||
Services/Repositories.
|
Services/Repositories.
|
||||||
- Zentrales Bootstrap fuer Config, DB-Verbindung, Session und Fehlerbehandlung.
|
- Zentrales Bootstrap fuer Config, DB-Verbindung, Session und Fehlerbehandlung.
|
||||||
- Layouts trennen: Public-Layout und App-Layout.
|
Begonnen mit `app/bootstrap.php`.
|
||||||
|
- Versionierte Migrationen einfuehren. Begonnen mit
|
||||||
|
`database/migrations/0001_legacy_mysql_baseline.sql` und
|
||||||
|
`scripts/migrate.php`.
|
||||||
|
- Layouts trennen: Public-Layout und App-Layout. Noch offen; Header/Footer
|
||||||
|
bleiben vorerst Legacy-Wrapper.
|
||||||
- Bestehende Assets weiterverwenden.
|
- Bestehende Assets weiterverwenden.
|
||||||
- CSRF- und Session-Basis einziehen.
|
- CSRF- und Session-Basis einziehen. Session und CSRF-Helper sind vorhanden;
|
||||||
|
globale Erzwingung erfolgt schrittweise pro POST-Seite.
|
||||||
- Konfigurationswerte aus Code in Umgebung oder Settings verschieben.
|
- Konfigurationswerte aus Code in Umgebung oder Settings verschieben.
|
||||||
|
|
||||||
Ergebnis:
|
Ergebnis:
|
||||||
|
|
||||||
- Grundgeruest fuer neue SaaS-App.
|
- Grundgeruest fuer neue SaaS-App.
|
||||||
- Kein fachlicher Rewrite, aber klare Struktur fuer die Migration.
|
- Kein fachlicher Rewrite, aber klare Struktur fuer die Migration.
|
||||||
|
- Dokumentation: `docs/m2-technical-foundation.md`.
|
||||||
|
|
||||||
Abhaengigkeiten:
|
Abhaengigkeiten:
|
||||||
|
|
||||||
- Entscheidung, ob PHP nativ weitergefuehrt oder ein Framework genutzt wird.
|
- Entscheidung, ob PHP nativ weitergefuehrt oder ein Framework genutzt wird.
|
||||||
|
- M2 bleibt bewusst ohne Tenant-/User-/Rollen-Tabellen; diese starten in M3.
|
||||||
|
|
||||||
### M3: Mandanten, Registrierung und Login
|
### M3: Mandanten, Registrierung und Login
|
||||||
|
|
||||||
|
|||||||
+71
-5
@@ -31,12 +31,78 @@ function dev_pdo(): PDO
|
|||||||
|
|
||||||
function dev_apply_schema(PDO $pdo): void
|
function dev_apply_schema(PDO $pdo): void
|
||||||
{
|
{
|
||||||
$schema = file_get_contents(__DIR__ . '/../database/mysql-dev-schema.sql');
|
dev_apply_migrations($pdo);
|
||||||
if ($schema === false) {
|
}
|
||||||
fwrite(STDERR, "Could not read schema file.\n");
|
|
||||||
|
function dev_migrations_dir(): string
|
||||||
|
{
|
||||||
|
return __DIR__ . '/../database/migrations';
|
||||||
|
}
|
||||||
|
|
||||||
|
function dev_ensure_migration_table(PDO $pdo): void
|
||||||
|
{
|
||||||
|
$pdo->exec(
|
||||||
|
'CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||||
|
version VARCHAR(255) PRIMARY KEY,
|
||||||
|
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, true>
|
||||||
|
*/
|
||||||
|
function dev_applied_migrations(PDO $pdo): array
|
||||||
|
{
|
||||||
|
dev_ensure_migration_table($pdo);
|
||||||
|
|
||||||
|
$applied = [];
|
||||||
|
$stmt = $pdo->query('SELECT version FROM schema_migrations ORDER BY version');
|
||||||
|
foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $version) {
|
||||||
|
$applied[(string)$version] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $applied;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string> Applied migration filenames.
|
||||||
|
*/
|
||||||
|
function dev_apply_migrations(PDO $pdo): array
|
||||||
|
{
|
||||||
|
$dir = dev_migrations_dir();
|
||||||
|
$files = glob($dir . '/*.sql');
|
||||||
|
if ($files === false || $files === []) {
|
||||||
|
fwrite(STDERR, "No migration files found in {$dir}.\n");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
$pdo->exec($schema);
|
sort($files, SORT_STRING);
|
||||||
}
|
$applied = dev_applied_migrations($pdo);
|
||||||
|
$newlyApplied = [];
|
||||||
|
$insert = $pdo->prepare('INSERT INTO schema_migrations (version) VALUES (?)');
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$version = basename($file);
|
||||||
|
if (isset($applied[$version])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = file_get_contents($file);
|
||||||
|
if ($sql === false) {
|
||||||
|
fwrite(STDERR, "Could not read migration file {$version}.\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->exec($sql);
|
||||||
|
$insert->execute([$version]);
|
||||||
|
$newlyApplied[] = $version;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
fwrite(STDERR, "Migration {$version} failed: {$e->getMessage()}\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $newlyApplied;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,35 +2,18 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
$required = ['DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASS', 'DEV_AUTH_EMAIL'];
|
require __DIR__ . '/dev-db.php';
|
||||||
foreach ($required as $name) {
|
|
||||||
if ((getenv($name) ?: '') === '') {
|
|
||||||
fwrite(STDERR, "Missing environment variable: {$name}\n");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$dsn = sprintf(
|
$email = getenv('DEV_AUTH_EMAIL');
|
||||||
'mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4',
|
if ($email === false || trim($email) === '') {
|
||||||
getenv('DB_HOST'),
|
fwrite(STDERR, "Missing environment variable: DEV_AUTH_EMAIL\n");
|
||||||
getenv('DB_PORT') ?: '3306',
|
|
||||||
getenv('DB_NAME')
|
|
||||||
);
|
|
||||||
|
|
||||||
$pdo = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$schema = file_get_contents(__DIR__ . '/../database/mysql-dev-schema.sql');
|
|
||||||
if ($schema === false) {
|
|
||||||
fwrite(STDERR, "Could not read schema file.\n");
|
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
$pdo->exec($schema);
|
$pdo = dev_pdo();
|
||||||
|
dev_apply_schema($pdo);
|
||||||
|
|
||||||
$email = strtolower(trim((string)getenv('DEV_AUTH_EMAIL')));
|
$email = strtolower(trim((string)$email));
|
||||||
$name = trim((string)(getenv('DEV_AUTH_NAME') ?: 'Test Admin'));
|
$name = trim((string)(getenv('DEV_AUTH_NAME') ?: 'Test Admin'));
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
@@ -64,4 +47,3 @@ if ($noticeCount === 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
echo "MySQL dev schema is ready for {$email}.\n";
|
echo "MySQL dev schema is ready for {$email}.\n";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/dev-db.php';
|
||||||
|
|
||||||
|
$pdo = dev_pdo();
|
||||||
|
$applied = dev_apply_migrations($pdo);
|
||||||
|
|
||||||
|
if ($applied === []) {
|
||||||
|
echo "No new migrations. Database is up to date.\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($applied as $version) {
|
||||||
|
echo "Applied migration: {$version}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Applied " . count($applied) . " migration(s).\n";
|
||||||
|
|
||||||
Reference in New Issue
Block a user