Stand vor Einführung des Nacht-Agenten

This commit is contained in:
Kundencenter 2026-09-27 00:51:32 +02:00
commit 4763548bfb
168 changed files with 12726 additions and 0 deletions

View file

@ -0,0 +1,50 @@
# Lizenzsystem-Erweiterung: Edition (Schlüssel-Präfix) und festes Ablaufdatum
**Zweck:** Das Kundencenter soll Familytool-Stufen verkaufen (Premium, Unlimited, Lifetime, Trial). Das Familytool erkennt die Stufe am **Präfix des Lizenzschlüssels** (`PREMIUM-…`, `TRIAL-…`, `LIFETIME…`, sonst Unlimited). Das Lizenzsystem konnte bisher nur Schlüssel ohne Präfix erzeugen, jede Lizenz wäre also automatisch Unlimited gewesen.
**Wo einspielen:** auf der Instanz, gegen die das Kundencenter und das Familytool laufen, also **licensing.flessinglabs.com** (nicht auf dem lokalen Dienst dieses Servers). Ohne den Patch verweigert das Kundencenter Bestellungen mit Edition (das Produkt bleibt Entwurf, eine Bestellung würde nie eine falsche Lizenz anlegen).
Getestet an einer Kopie des Codes mit eigener Testdatenbank auf einem separaten Port: Präfix für alle drei Editionen, ungültiges Präfix → 422, festes Ablaufdatum, unveränderte Anlage ohne Präfix, `validate` mit Präfix-Schlüssel, Erkennung über `GET /`.
## Ablauf (ca. 10 Minuten, kurze Unterbrechung nur beim Neustart)
1. **Sichern** (DB und Code des Lizenzsystems auf dem Zielserver):
```bash
mysqldump --single-transaction <DBNAME> > licensing-vor-erweiterung.sql
cp -a <pfad>/backend/app app-vor-erweiterung
```
2. **Vorab prüfen: startet der Dienst nach einem Neustart korrekt?** (Wir hatten am 26.09. auf dem lokalen Dienst einen Ausfall, weil das venv neuer war als der laufende Prozess. Siehe LICENSING-11.)
```bash
<pfad>/backend/venv/bin/pip list | grep -iE "bcrypt|passlib|slowapi|starlette" # bcrypt muss < 5 sein (z. B. 4.0.1)
grep -n "def validate_license" -A3 <pfad>/backend/app/routers/licenses.py # Parameter muss "request: Request" heißen
```
Wenn `bcrypt` ≥ 5 ist oder die Schema-Parameter noch `request` heißen: **erst das beheben**, sonst fallen Login und `validate` beim Neustart aus.
3. **Datenbank erweitern** (Schlüssel werden mit Präfix länger als 36 Zeichen):
```sql
ALTER TABLE licenses MODIFY license_key VARCHAR(64) NOT NULL;
```
4. **Code anpassen** (erst Trockenlauf, dann anwenden):
```bash
python3 apply_erweiterung.py <pfad>/backend # zeigt nur, was geändert würde
python3 apply_erweiterung.py <pfad>/backend --apply # legt Sicherungen *.bak-<zeit> an
```
Weicht der Code ab, bricht das Skript ohne Änderung ab und nennt die Stelle.
5. **Dienst neu starten** (z. B. `systemctl restart licensing-backend`) und prüfen:
```bash
curl -s https://licensing.flessinglabs.com/api/ # muss "features":["key_prefix","explicit_expiry"] enthalten
```
Danach kurz testen: Login im Admin-Panel, eine Lizenz prüfen (`validate`) und im Familytool die Lizenzprüfung.
6. **Im Kundencenter**: Verbindungen → „Jetzt abgleichen“. Unter „Fähigkeiten“ erscheinen `license.key_prefix` und `license.expiry`. Danach lassen sich die Familytool-Produkte aktivieren.
## Zurückrollen
Dienst stoppen, `*.bak-<zeit>` zurückkopieren, Dienst starten. Die Spalte darf auf 64 Zeichen bleiben. Bereits mit Präfix angelegte Lizenzen bleiben gültig.
## API-Änderungen im Detail
- `POST /licenses/` Body: neues optionales Feld `key_prefix` (`PREMIUM` | `TRIAL` | `LIFETIME`). Antwort enthält den Schlüssel `<PRAEFIX>-<uuid>`.
- `POST /licenses/`: ein angegebenes `expires_at` (Ortszeit wie bisher) wird verwendet; ohne Angabe gilt die bisherige Berechnung aus `duration_type`.
- `GET /`: Feld `features` (Liste).
- Nicht geändert: `validate`, `register`, Shop, Login, alle bestehenden Felder.
## Bekannte Grenzen
- Ein Upgrade von Premium auf Unlimited bedeutet einen **neuen Schlüssel** (das Präfix steckt im Schlüssel). Das Kundencenter legt dann eine neue Lizenz an; die alte wird gesperrt. Eine spätere Lösung mit einem Feld `edition` im Lizenzsystem (statt Präfix) ist möglich.
- Das Ablaufdatum wird (wie im Lizenzsystem üblich) als Ortszeit gespeichert.

View file

@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""
Erweiterung des Lizenzsystems um "Edition" (Schlüssel-Präfix) und festes Ablaufdatum – für das Kundencenter.
Was der Patch tut (alles abwärtskompatibel, bestehende Clients ändern sich nicht):
1. POST /licenses/ akzeptiert optional key_prefix: "PREMIUM" | "TRIAL" | "LIFETIME"
-> Schlüssel lautet dann "<PRAEFIX>-<uuid>" (das Familytool erkennt daran die Stufe; ohne Präfix = Unlimited).
2. POST /licenses/ beachtet ein ausdrücklich angegebenes expires_at (bisher wurde es ignoriert).
3. GET / meldet "features": ["key_prefix", "explicit_expiry"] (daran erkennt das Kundencenter die Erweiterung).
4. models.License.license_key wird von 36 auf 64 Zeichen erweitert (DB-Änderung: siehe README, VORHER ausführen).
Nutzung (Standard = nur prüfen, nichts ändern):
python3 apply_erweiterung.py /pfad/zu/licensing-system/backend # Trockenlauf
python3 apply_erweiterung.py /pfad/zu/licensing-system/backend --apply # anwenden (mit Sicherungskopien *.bak-<zeit>)
Das Skript ist idempotent: bereits vorhandene Änderungen werden erkannt und übersprungen.
"""
import re, sys, time, py_compile, pathlib
def main() -> int:
args = [a for a in sys.argv[1:] if not a.startswith('--')]
apply = '--apply' in sys.argv
if len(args) != 1:
print(__doc__); return 2
root = pathlib.Path(args[0]) / 'app'
files = {n: root / p for n, p in {'models': 'models.py', 'schemas': 'schemas.py', 'licenses': 'routers/licenses.py', 'main': 'main.py'}.items()}
for n, f in files.items():
if not f.exists():
print(f'FEHLER: {f} nicht gefunden – falscher Pfad? (erwartet: .../licensing-system/backend)'); return 1
src = {n: f.read_text() for n, f in files.items()}
new = dict(src); report = []
def step(name, key, old, repl, marker):
if marker in new[key]:
report.append(f' = {name}: bereits vorhanden'); return True
if old not in new[key]:
report.append(f' ! {name}: erwartete Stelle NICHT gefunden – bitte manuell prüfen (siehe README)'); return False
new[key] = new[key].replace(old, repl, 1); report.append(f' + {name}'); return True
ok = True
ok &= step('models: license_key 64 Zeichen', 'models',
'license_key = Column(String(36), unique=True, index=True, default=lambda: str(uuid.uuid4()))',
'license_key = Column(String(64), unique=True, index=True, default=lambda: str(uuid.uuid4()))', 'String(64), unique=True')
ok &= step('schemas: key_prefix in LicenseCreate', 'schemas',
'class LicenseCreate(LicenseBase):\n pass',
'class LicenseCreate(LicenseBase):\n # Optionales Schlüssel-Präfix (Edition): Schlüssel lautet "<PRAEFIX>-<uuid>". Ohne Präfix gilt die Lizenz im Familytool als UNLIMITED.\n key_prefix: Optional[Literal["PREMIUM", "TRIAL", "LIFETIME"]] = None',
'key_prefix: Optional[Literal')
if 'Literal' not in re.split(r'\nclass ', new['schemas'], maxsplit=1)[0]:
m = re.search(r'^from typing import (.+)$', new['schemas'], re.M)
new['schemas'] = new['schemas'].replace(m.group(0), f'from typing import Literal, {m.group(1)}', 1) if m else 'from typing import Literal\n' + new['schemas']
report.append(' + schemas: Literal importiert')
ok &= step('licenses: festes Ablaufdatum hat Vorrang', 'licenses',
' # Calculate expires_at based on duration_type\n expires_at = None\n if license.duration_type != schemas.LicenseDurationType.UNLIMITED:',
' # Ein ausdrücklich angegebenes Ablaufdatum hat Vorrang; sonst wird es aus duration_type berechnet\n expires_at = license.expires_at\n if expires_at is None and license.duration_type != schemas.LicenseDurationType.UNLIMITED:',
'expires_at = license.expires_at')
ok &= step('licenses: Schlüssel mit Präfix', 'licenses',
' db_license = models.License(\n program_id=license.program_id,',
' extra = {"license_key": f"{license.key_prefix}-{uuid.uuid4()}"} if license.key_prefix else {}\n db_license = models.License(\n **extra,\n program_id=license.program_id,',
'f"{license.key_prefix}-')
if not re.search(r'^import uuid$', new['licenses'], re.M):
new['licenses'] = re.sub(r'^import datetime$', 'import datetime\nimport uuid', new['licenses'], count=1, flags=re.M); report.append(' + licenses: uuid importiert')
ok &= step('main: features in GET /', 'main',
'return {"message": "Licensing System Backend is running!"}',
'return {"message": "Licensing System Backend is running!", "features": ["key_prefix", "explicit_expiry"]}', '"features"')
print('\n'.join(report))
if not ok:
print('\nABBRUCH: Nicht alle Stellen passen (Code weicht ab). Es wurde NICHTS geändert.'); return 1
changed = [n for n in files if new[n] != src[n]]
if not changed:
print('\nNichts zu tun – die Erweiterung ist bereits vollständig eingespielt.'); return 0
if not apply:
print(f'\nTROCKENLAUF: würde {", ".join(files[n].name for n in changed)} ändern. Mit --apply ausführen.'); return 0
ts = time.strftime('%Y%m%d-%H%M%S')
for n in changed:
files[n].with_name(files[n].name + f'.bak-{ts}').write_text(src[n])
files[n].write_text(new[n])
py_compile.compile(str(files[n]), doraise=True)
print(f'\nANGEWENDET: {", ".join(files[n].name for n in changed)} (Sicherungen: *.bak-{ts}). Jetzt Dienst neu starten (siehe README).')
return 0
if __name__ == '__main__':
sys.exit(main())