diff --git a/.env b/.env index 90504b3..f5fca0c 100644 --- a/.env +++ b/.env @@ -72,3 +72,12 @@ GITHUB_TOKEN= # Optional: Gitea Access Token for private instances GITEA_TOKEN= ###< app/git-services ### + +###> Plugin-System ### +# Lizenz-Backend: LicenseValidator oder GiteaLicenseValidator +LICENSE_BACKEND=LicenseValidator +LICENSE_SERVER_URL=https://license.example.com +GITEA_BASE_URL=https://git.example.com +GITEA_ORGANIZATION=mycrm +INSTANCE_ID=changeme +###< Plugin-System ### diff --git a/.env.plugin.example b/.env.plugin.example index 153257e..7a98051 100644 --- a/.env.plugin.example +++ b/.env.plugin.example @@ -3,9 +3,26 @@ ###> Plugin-System ### +# Wähle Lizenzierungs-Backend: "LicenseValidator" (Standard) oder "GiteaLicenseValidator" +# Standard: REST-API basierter Lizenzserver +# Gitea: Nutzt Gitea Repository-Access als Lizenzierung +LICENSE_BACKEND=LicenseValidator + +# === Standard Lizenzserver (LICENSE_BACKEND=LicenseValidator) === + # URL des Lizenzservers (ohne trailing slash) LICENSE_SERVER_URL=https://license.mycrm.local +# === Gitea Lizenzserver (LICENSE_BACKEND=GiteaLicenseValidator) === + +# Gitea Base URL (ohne trailing slash) +GITEA_BASE_URL=https://git.mycrm.local + +# Gitea Organisation oder User, der die Modul-Repositories besitzt +GITEA_ORGANIZATION=mycrm + +# === Gemeinsame Konfiguration === + # Eindeutige Instance-ID (generiere mit: php bin/console app:generate-instance-id # oder verwende: uuidgen / openssl rand -hex 16) INSTANCE_ID=your-unique-instance-identifier-here @@ -13,7 +30,9 @@ INSTANCE_ID=your-unique-instance-identifier-here ###< Plugin-System ### ###> Modul-Lizenzen ### -# Format: LICENSE_{MODULE_IDENTIFIER}=lizenzschlüssel + +# === Für Standard-Lizenzserver (LICENSE_BACKEND=LicenseValidator) === +# Format: LICENSE_{MODULE_IDENTIFIER}=JWT-Token-vom-Lizenzserver # Beispiel: Billing-Modul # LICENSE_BILLING=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... @@ -21,8 +40,18 @@ INSTANCE_ID=your-unique-instance-identifier-here # Beispiel: Invoicing-Modul # LICENSE_INVOICING=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... -# Beispiel: Inventory-Modul -# LICENSE_INVENTORY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +# === Für Gitea-Lizenzserver (LICENSE_BACKEND=GiteaLicenseValidator) === +# Format: GITEA_TOKEN_{MODULE_IDENTIFIER}=gitea-access-token + +# Beispiel: Billing-Modul +# GITEA_TOKEN_BILLING=abc123def456... + +# Beispiel: Invoicing-Modul +# GITEA_TOKEN_INVOICING=xyz789ghi012... + +# Hinweis: Gitea Access Token generieren unter: +# Gitea → Settings → Applications → Generate New Token +# Erforderliche Scopes: repo (read access) ###< Modul-Lizenzen ### diff --git a/.gitignore b/.gitignore index a666528..f62dea1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ npm-debug.log yarn-error.log ###< symfony/webpack-encore-bundle ### +auth.json diff --git a/composer.json b/composer.json index a39d08f..331069e 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "doctrine/orm": "^3.5", "knpuniversity/oauth2-client-bundle": "*", "league/oauth2-client": "*", + "mycrm/test-module": "*", "nelmio/cors-bundle": "^2.6", "phpdocumentor/reflection-docblock": "^5.6", "phpstan/phpdoc-parser": "^2.3", @@ -112,5 +113,11 @@ "symfony/maker-bundle": "^1.0", "symfony/stopwatch": "7.1.*", "symfony/web-profiler-bundle": "7.1.*" + }, + "repositories": { + "mycrm-test-module": { + "type": "vcs", + "url": "https://git.osdata-home.de/mycrm/mycrm-test-module" + } } } diff --git a/composer.lock b/composer.lock index 0608a85..96afb53 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c70cc2a152b707d0dcf4c562bd06f8d5", + "content-hash": "caa943b57e0e058e754382b31531775b", "packages": [ { "name": "api-platform/core", @@ -2038,6 +2038,42 @@ ], "time": "2025-03-24T10:02:05+00:00" }, + { + "name": "mycrm/test-module", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://git.osdata-home.de/mycrm/mycrm-test-module", + "reference": "0630a840ffdca475208b51f7807196468e44c27c" + }, + "require": { + "php": ">=8.2", + "symfony/framework-bundle": "^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev", + "stable": "v1.0.0" + } + }, + "autoload": { + "psr-4": { + "MyCRM\\TestModule\\": "src/" + } + }, + "license": [ + "proprietary" + ], + "authors": [ + { + "name": "Your Name", + "email": "your.email@example.com" + } + ], + "description": "Test Module for myCRM - Demonstrates the plugin system", + "time": "2025-12-03T15:57:07+00:00" + }, { "name": "nelmio/cors-bundle", "version": "2.6.0", diff --git a/config/bundles.php b/config/bundles.php index 4f7d9df..d6b4866 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -18,4 +18,5 @@ return [ Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true], + MyCRM\TestModule\TestModuleBundle::class => ['all' => true], ]; diff --git a/config/services.yaml b/config/services.yaml index dc617b3..0439042 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -14,6 +14,11 @@ services: _defaults: autowire: true # Automatically injects dependencies in your services. autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + bind: + $licenseServerUrl: '%env(LICENSE_SERVER_URL)%' + $giteaBaseUrl: '%env(GITEA_BASE_URL)%' + $giteaOrganization: '%env(GITEA_ORGANIZATION)%' + $instanceId: '%env(INSTANCE_ID)%' # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name diff --git a/config/services_plugin.yaml b/config/services_plugin.yaml index 8852491..7fafc70 100644 --- a/config/services_plugin.yaml +++ b/config/services_plugin.yaml @@ -1,13 +1,20 @@ # Plugin-System Services Configuration services: - # Lizenz-Validator - App\Plugin\LicenseValidatorInterface: - class: App\Service\LicenseValidator - arguments: + _defaults: + autowire: true + autoconfigure: true + bind: $licenseServerUrl: '%env(LICENSE_SERVER_URL)%' + $giteaBaseUrl: '%env(GITEA_BASE_URL)%' + $giteaOrganization: '%env(GITEA_ORGANIZATION)%' $instanceId: '%env(INSTANCE_ID)%' + # Interface-Alias: Verwendet Gitea-Validator + # Um auf Standard-Validator zu wechseln: Ändere zu @App\Service\LicenseValidator + App\Plugin\LicenseValidatorInterface: + alias: 'App\Service\GiteaLicenseValidator' + # Module Registry (automatisches TaggedIterator sammelt alle Plugins) App\Plugin\ModuleRegistry: arguments: diff --git a/docs/GITEA_LICENSE_SYSTEM.md b/docs/GITEA_LICENSE_SYSTEM.md new file mode 100644 index 0000000..94a7e8f --- /dev/null +++ b/docs/GITEA_LICENSE_SYSTEM.md @@ -0,0 +1,500 @@ +# Gitea-basiertes Lizenzierungs-System + +## Übersicht + +Das Gitea-basierte Lizenzierungs-System nutzt **Gitea Repository-Zugriff** als Lizenzierung für myCRM-Module. Anstatt einen separaten Lizenzserver zu betreiben, prüft das System, ob ein Benutzer Zugriff auf das private Gitea-Repository eines Moduls hat. + +## Konzept + +``` +Gitea Access Token → Repository-Zugriff → Gültige Lizenz +``` + +### Vorteile + +✅ **Keine separate Lizenzserver-API nötig** - Nutzt bestehende Gitea-Infrastruktur +✅ **Einfache Verwaltung** - Access Control über Gitea UI +✅ **Flexibel** - Granulare Berechtigungen pro Repository +✅ **Sicher** - Nutzt Gitea's bewährte Authentifizierung +✅ **Offline-Support** - 24h Cache + 7 Tage Grace Period +✅ **Composer-Integration** - Gleiche Tokens für Composer und Lizenzierung + +### Funktionsweise + +1. **Modul-Repositories in Gitea erstellen** (z.B. `mycrm/mycrm-billing-module`) +2. **Private Repositories** - Nur lizenzierte Benutzer haben Zugriff +3. **Gitea Access Token** als Lizenzschlüssel verwenden +4. **myCRM prüft Repository-Zugriff** über Gitea API +5. **Zugriff = Lizenz gültig** + +--- + +## Installation & Konfiguration + +### 1. Gitea-Backend aktivieren + +```bash +# .env.local +LICENSE_BACKEND=GiteaLicenseValidator +GITEA_BASE_URL=https://git.mycrm.local +GITEA_ORGANIZATION=mycrm +INSTANCE_ID=deine-unique-instance-id +``` + +### 2. Modul-Repository in Gitea erstellen + +**Namenskonvention:** `mycrm-{module-identifier}-module` + +Beispiele: +- Billing-Modul: `mycrm-billing-module` +- Invoicing-Modul: `mycrm-invoicing-module` +- Inventory-Modul: `mycrm-inventory-module` + +**Repository-Settings:** +- **Visibility:** Private +- **Owner:** Deine Organisation oder dein User (z.B. `mycrm`) + +### 3. Gitea Access Token generieren + +1. Gehe zu **Gitea → Settings → Applications** +2. Klicke auf **"Generate New Token"** +3. Name: `myCRM License - {Modul-Name}` +4. **Scopes auswählen:** + - ✅ `repo` (Read access to repositories) +5. Token kopieren (wird nur einmal angezeigt!) + +### 4. Lizenz in myCRM registrieren + +#### Option A: CLI (empfohlen) +```bash +php bin/console app:module:license billing YOUR_GITEA_TOKEN_HERE +``` + +#### Option B: Manuell in .env.local +```bash +# .env.local +GITEA_TOKEN_BILLING=abc123def456... +GITEA_TOKEN_INVOICING=xyz789ghi012... +``` + +### 5. Verifizieren + +```bash +php bin/console app:module:list +``` + +Ausgabe sollte zeigen: +``` +billing ✓ Aktiv Gitea Repository-Zugriff bestätigt (mycrm/mycrm-billing-module) +``` + +--- + +## Repository-Metadaten für Lizenzinformationen + +Das System kann Lizenzinformationen aus Repository-Metadaten extrahieren: + +### Features über Topics + +Füge Topics zum Repository hinzu, um Features zu definieren: + +``` +Topics: feature-invoices, feature-payments, feature-reports +``` + +Diese werden als `features` im Lizenz-Array zurückgegeben: +```php +[ + 'valid' => true, + 'features' => ['invoices', 'payments', 'reports'], + // ... +] +``` + +### Ablaufdatum in Description + +Füge das Ablaufdatum in die Repository-Description ein: + +``` +Premium Billing Module for myCRM +License expires: 2025-12-31 +``` + +Unterstützte Formate: +- `expires: YYYY-MM-DD` +- `valid until: YYYY-MM-DD` +- `License expires: YYYY-MM-DD` + +Das System parsed automatisch das Datum: +```php +[ + 'valid' => true, + 'expiresAt' => DateTimeImmutable('2025-12-31'), + // ... +] +``` + +--- + +## Access Control: Wer darf welches Modul nutzen? + +### Variante 1: Team-Zugriff per Gitea Teams + +1. **Team in Gitea erstellen** + - Gehe zu Organisation → Teams → "Create Team" + - Name: `Premium Customers` + +2. **Team-Mitglieder hinzufügen** + - Team → Members → "Add Member" + +3. **Repository dem Team zuweisen** + - Repository → Settings → Collaboration + - Add Team: `Premium Customers` mit **Read** Permission + +4. **Jedes Team-Mitglied erhält Zugriff** + - Alle Mitglieder können nun Tokens mit Repo-Zugriff generieren + +### Variante 2: Individuelle Collaborators + +1. **Repository → Settings → Collaboration** +2. **"Add Collaborator"** +3. User auswählen + **Read** Permission +4. User kann jetzt Token mit Zugriff generieren + +### Variante 3: Organisation-weiter Zugriff + +1. **Organisation → Settings → Members** +2. Mitglieder mit **Read**-Rechten auf alle Private Repos +3. Alle Mitglieder haben automatisch Zugriff auf alle Module + +--- + +## Composer Integration + +Da Gitea-Tokens sowohl für Lizenzierung als auch für Composer-Installation genutzt werden können, ist die Integration nahtlos: + +### composer.json konfigurieren + +```bash +composer config repositories.mycrm-billing vcs https://git.mycrm.local/mycrm/mycrm-billing-module +``` + +### auth.json konfigurieren + +```json +{ + "http-basic": { + "git.mycrm.local": { + "username": "dein-gitea-username", + "password": "DEIN_GITEA_TOKEN" + } + } +} +``` + +### Modul installieren + +```bash +composer require mycrm/billing-module +``` + +**Ein Token für beides:** +- ✅ Composer Installation +- ✅ Lizenz-Validierung + +--- + +## CLI-Befehle + +### Lizenz registrieren + +```bash +php bin/console app:module:license + +# Beispiel +php bin/console app:module:license billing abc123def456ghi789 +``` + +### Alle Module auflisten + +```bash +php bin/console app:module:list + +# Ausgabe: +# ┌───────────┬────────┬─────────────────────────────────────────────┐ +# │ Modul │ Status │ Lizenz-Info │ +# ├───────────┼────────┼─────────────────────────────────────────────┤ +# │ billing │ ✓ Aktiv│ Gitea Repository-Zugriff (mycrm/mycrm-...) │ +# │ invoicing │ ✗ Inakt│ Kein Gitea Access Token vorhanden │ +# └───────────┴────────┴─────────────────────────────────────────────┘ +``` + +### Lizenz widerrufen + +```bash +php bin/console app:module:revoke billing +``` + +--- + +## Caching & Offline-Betrieb + +### Cache-Strategie + +``` +Online-Validierung → 24h Cache → 7 Tage Grace Period → Offline-Fallback +``` + +1. **Erste Validierung:** Gitea API-Call +2. **24 Stunden:** Gecacht, keine API-Calls +3. **Nach 24h:** Neue Online-Validierung +4. **Offline-Fall:** Grace Period (7 Tage ab letzter erfolgreicher Validierung) + +### Cache-Verhalten + +```php +// Bei jedem Request wird geprüft: +if (cache_valid && within_24h) { + return cached_license; +} + +// Online-Validierung +try { + $license = validate_with_gitea(); + cache($license, 24h); +} catch (NetworkError $e) { + // Fallback auf Cache mit Grace Period + if (cached_license && within_7_days) { + return cached_license; + } + throw LicenseException(); +} +``` + +### Vorteile + +- ✅ **Performance:** Nur 1 API-Call pro 24h +- ✅ **Offline-Betrieb:** 7 Tage funktionsfähig ohne Internet +- ✅ **Kein Dauerbetrieb offline:** Nach 7 Tagen ist Online-Validierung erforderlich + +--- + +## API-Endpunkte + +Das System stellt REST-API-Endpunkte für Admin-UIs bereit: + +### GET /api/modules + +Liste aller Module mit Lizenz-Status + +```json +{ + "modules": [ + { + "identifier": "billing", + "name": "Billing & Invoicing", + "version": "1.0.0", + "license": { + "valid": true, + "licensedTo": "Max Mustermann", + "expiresAt": "2025-12-31T23:59:59+00:00", + "message": "Gitea Repository-Zugriff bestätigt (mycrm/mycrm-billing-module)", + "features": ["invoices", "payments", "reports"] + } + } + ] +} +``` + +### POST /api/modules/{module}/license + +Lizenz registrieren + +```bash +curl -X POST https://mycrm.local/api/modules/billing/license \ + -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"licenseKey": "abc123def456..."}' +``` + +### DELETE /api/modules/{module}/license + +Lizenz widerrufen + +```bash +curl -X DELETE https://mycrm.local/api/modules/billing/license \ + -H "Authorization: Bearer YOUR_ADMIN_TOKEN" +``` + +--- + +## Security Best Practices + +### ✅ DO's + +- **Tokens niemals committen** - Nutze `.env.local` (nicht in Git) +- **Token-Scopes minimal halten** - Nur `repo:read` erforderlich +- **Tokens rotieren** - Regelmäßig neue Tokens generieren +- **Team-basierte Zugriffssteuerung** - Nutze Gitea Teams für Kunden-Gruppen +- **HTTPS verwenden** - Gitea nur über HTTPS erreichbar machen +- **Tokens pro Modul** - Separate Tokens für jedes Modul + +### ❌ DONT's + +- **Keine Tokens in composer.json** - Nur in `auth.json` +- **Keine universellen Tokens** - Nicht einen Token für alle Module +- **Keine öffentlichen Repositories** - Module müssen private sein +- **Keine Shared Tokens** - Jeder Kunde eigener Token + +--- + +## Troubleshooting + +### Problem: "Kein Zugriff auf Modul-Repository" + +**Ursachen:** +1. Token hat keine `repo:read` Berechtigung +2. Repository ist nicht vorhanden +3. User ist nicht Collaborator/Team-Mitglied +4. Repository-Name folgt nicht Namenskonvention + +**Lösung:** +```bash +# Token testen +curl -H "Authorization: token YOUR_TOKEN" \ + https://git.mycrm.local/api/v1/repos/mycrm/mycrm-billing-module + +# Sollte Status 200 zurückgeben +``` + +### Problem: "Gitea nicht erreichbar" + +**Symptom:** Modul läuft, zeigt aber "Offline-Modus (Grace Period)" + +**Ursache:** Gitea-Server nicht erreichbar, aber gecachte Lizenz noch gültig + +**Aktion:** Prüfe `GITEA_BASE_URL` in `.env.local` + +### Problem: "Grace Period abgelaufen" + +**Symptom:** Modul wird nicht gestartet + +**Ursache:** Letzte erfolgreiche Validierung > 7 Tage her + +**Lösung:** +1. Gitea-Server erreichbar machen +2. Cache clearen: `php bin/console cache:clear` +3. Modul neu starten + +--- + +## Migration: Standard-Lizenzserver → Gitea + +### Schritt 1: Bestehende Lizenzen notieren + +```bash +php bin/console app:module:list +``` + +### Schritt 2: Gitea-Repositories erstellen + +Für jedes lizenzierte Modul: +1. Repository anlegen: `mycrm-{modul}-module` +2. Als Private setzen +3. Kunden als Collaborators hinzufügen + +### Schritt 3: .env.local aktualisieren + +```bash +# Alt (entfernen oder auskommentieren) +#LICENSE_BACKEND=LicenseValidator +#LICENSE_SERVER_URL=https://license.mycrm.local +#LICENSE_BILLING=eyJhbGciOi... + +# Neu +LICENSE_BACKEND=GiteaLicenseValidator +GITEA_BASE_URL=https://git.mycrm.local +GITEA_ORGANIZATION=mycrm +GITEA_TOKEN_BILLING=abc123... +``` + +### Schritt 4: Cache leeren & testen + +```bash +php bin/console cache:clear +php bin/console app:module:list +``` + +--- + +## Erweiterte Konfiguration + +### Modul-spezifische Repository-Namen + +Standardmäßig: `mycrm-{module}-module` + +Für custom Namen, erweitere `GiteaLicenseValidator::getRepositoryName()`: + +```php +private function getRepositoryName(string $moduleIdentifier): string +{ + // Custom Mapping + $mapping = [ + 'billing' => 'premium-billing-suite', + 'invoicing' => 'invoice-pro', + ]; + + return $mapping[$moduleIdentifier] ?? 'mycrm-' . $moduleIdentifier . '-module'; +} +``` + +### Custom Feature-Extraction + +Erweitere `extractFeaturesFromRepository()` für eigene Logik: + +```php +private function extractFeaturesFromRepository(array $repoData): array +{ + // Beispiel: Features aus Repository-README parsen + // Beispiel: Features aus Gitea Webhooks abrufen + // ... +} +``` + +--- + +## Vergleich: Standard vs. Gitea + +| Feature | Standard-Lizenzserver | Gitea-Lizenzserver | +|---------|----------------------|-------------------| +| **Infrastruktur** | Eigene REST-API | Bestehende Gitea-Instanz | +| **Setup-Aufwand** | Hoch | Niedrig | +| **Verwaltung** | Custom UI | Gitea UI | +| **Token-Typ** | JWT | Gitea Access Token | +| **Access Control** | Custom-Logik | Gitea Permissions | +| **Composer-Integration** | Separat | Gleiche Tokens | +| **Offline-Support** | ✅ 7 Tage | ✅ 7 Tage | +| **Skalierung** | Custom | Gitea | + +--- + +## Fazit + +Das Gitea-basierte Lizenzierungs-System ist ideal, wenn: + +✅ Du bereits Gitea für Versionskontrolle nutzt +✅ Du keine separate Lizenzserver-API betreiben möchtest +✅ Du flexible, repository-basierte Access Control benötigst +✅ Du Composer-Installation und Lizenzierung vereinheitlichen willst + +**Nächste Schritte:** + +1. Gitea-Instanz einrichten (falls noch nicht vorhanden) +2. Private Modul-Repositories erstellen +3. `.env.local` konfigurieren +4. Erste Lizenz registrieren +5. Testen! + +**Fragen? Siehe auch:** +- `PLUGIN_SYSTEM.md` - Vollständige Plugin-System-Dokumentation +- `PLUGIN_QUICKSTART.md` - 5-Minuten-Setup diff --git a/docs/GITEA_QUICKSTART.md b/docs/GITEA_QUICKSTART.md new file mode 100644 index 0000000..bc5aa2c --- /dev/null +++ b/docs/GITEA_QUICKSTART.md @@ -0,0 +1,238 @@ +# Gitea-Lizenzierung: 5-Minuten Quickstart + +## Schritt 1: Repository in Gitea erstellen (1 Min) + +```bash +# In Gitea UI: +1. Klicke auf "+" → "New Repository" +2. Repository Name: mycrm-billing-module +3. Owner: mycrm (oder dein User) +4. ✅ Make Repository Private +5. Klicke "Create Repository" +``` + +**Repository-URL:** `https://git.mycrm.local/mycrm/mycrm-billing-module` + +--- + +## Schritt 2: Gitea Access Token generieren (1 Min) + +```bash +# In Gitea UI: +1. Klicke auf dein Avatar → "Settings" +2. Linke Sidebar → "Applications" +3. Section "Generate New Token" + - Token Name: "myCRM License - Billing Module" + - ✅ Select scopes: repo +4. Klicke "Generate Token" +5. 📋 Kopiere den Token (wird nur einmal angezeigt!) +``` + +**Beispiel-Token:** `abc123def456ghi789jkl012mno345` + +--- + +## Schritt 3: myCRM konfigurieren (1 Min) + +```bash +# .env.local erstellen/bearbeiten +cp .env.plugin.example .env.local +nano .env.local +``` + +**Konfiguration:** +```bash +###> Plugin-System ### +LICENSE_BACKEND=GiteaLicenseValidator +GITEA_BASE_URL=https://git.mycrm.local +GITEA_ORGANIZATION=mycrm +INSTANCE_ID=meine-crm-instance-1 + +###> Modul-Lizenzen ### +GITEA_TOKEN_BILLING=abc123def456ghi789jkl012mno345 +``` + +**Speichern:** `Ctrl+O` → `Enter` → `Ctrl+X` + +--- + +## Schritt 4: Modul installieren (1 Min) + +```bash +# Composer Repository konfigurieren +composer config repositories.mycrm-billing vcs https://git.mycrm.local/mycrm/mycrm-billing-module + +# Composer Auth konfigurieren (gleicher Token!) +composer config http-basic.git.mycrm.local dein-username abc123def456ghi789jkl012mno345 + +# Modul installieren +composer require mycrm/billing-module + +# Migration ausführen +php bin/console doctrine:migrations:migrate -n +``` + +--- + +## Schritt 5: Verifizieren (1 Min) + +```bash +# Cache leeren +php bin/console cache:clear + +# Module auflisten +php bin/console app:module:list +``` + +**Erwartete Ausgabe:** +``` +┌──────────┬─────────┬────────────────────────────────────────────┐ +│ Modul │ Status │ Lizenz-Info │ +├──────────┼─────────┼────────────────────────────────────────────┤ +│ billing │ ✓ Aktiv │ Gitea Repository-Zugriff bestätigt │ +│ │ │ (mycrm/mycrm-billing-module) │ +└──────────┴─────────┴────────────────────────────────────────────┘ +``` + +--- + +## ✅ Fertig! + +Dein Modul ist jetzt lizenziert und einsatzbereit. + +**Testen:** +1. Gehe zu `https://mycrm.local/billing` +2. Das Modul sollte geladen sein +3. Bei Problemen: Logs prüfen in `var/log/dev.log` + +--- + +## Weitere Benutzer lizenzieren + +### Variante A: Gitea Collaborator hinzufügen + +```bash +# In Gitea UI: +1. Gehe zu Repository: mycrm-billing-module +2. Settings → Collaboration +3. Klicke "Add Collaborator" +4. User suchen und auswählen +5. Permission: Read +6. Klicke "Add Collaborator" +``` + +Der neue User kann jetzt: +1. Eigenen Token generieren +2. In seiner myCRM-Instanz den Token eintragen +3. Modul nutzen + +### Variante B: Gitea Team erstellen (für mehrere User) + +```bash +# In Gitea UI: +1. Gehe zu Organisation "mycrm" +2. Teams → "Create Team" +3. Team Name: "Premium Customers" +4. Permissions: Read +5. Klicke "Create Team" + +# Dann: Team-Mitglieder hinzufügen +1. Team → Members +2. "Add Team Member" +3. User auswählen + +# Dann: Repository dem Team zuweisen +1. Gehe zu Repository: mycrm-billing-module +2. Settings → Collaboration +3. "Add Team" +4. Team: Premium Customers +5. Permission: Read +``` + +Alle Team-Mitglieder haben automatisch Zugriff! + +--- + +## Troubleshooting + +### ❌ "Kein Zugriff auf Modul-Repository" + +**Test:** +```bash +curl -H "Authorization: token abc123def456..." \ + https://git.mycrm.local/api/v1/repos/mycrm/mycrm-billing-module +``` + +**Sollte zurückgeben:** Status 200 + JSON + +**Falls Status 404:** +- Repository existiert nicht oder ist falsch benannt +- Prüfe: https://git.mycrm.local/mycrm/mycrm-billing-module + +**Falls Status 401:** +- Token ist ungültig oder hat keine `repo` Berechtigung +- Generiere neuen Token mit korrekten Scopes + +### ❌ Modul erscheint nicht in Liste + +```bash +# Cache leeren +php bin/console cache:clear + +# Plugin-System neu laden +php bin/console cache:warmup + +# Prüfen ob .env.local geladen wird +php bin/console debug:container --env-vars | grep GITEA +``` + +### ❌ Composer kann nicht installieren + +```bash +# Auth prüfen +cat auth.json + +# Sollte enthalten: +{ + "http-basic": { + "git.mycrm.local": { + "username": "dein-username", + "password": "dein-token" + } + } +} + +# Neu versuchen +composer install -vvv +``` + +--- + +## Nächste Schritte + +- **Weitere Module lizenzieren:** Wiederhole Schritte 1-5 +- **Admin-UI nutzen:** Gehe zu `/admin/modules` für grafische Verwaltung +- **Lizenz-Metadaten:** Lies `GITEA_LICENSE_SYSTEM.md` für Features & Ablaufdatum +- **Vollständige Doku:** Siehe `PLUGIN_SYSTEM.md` + +--- + +## Cheat Sheet + +```bash +# Token registrieren +php bin/console app:module:license billing YOUR_TOKEN + +# Status prüfen +php bin/console app:module:list + +# Lizenz widerrufen +php bin/console app:module:revoke billing + +# Cache leeren +php bin/console cache:clear + +# Gitea API testen +curl -H "Authorization: token YOUR_TOKEN" \ + https://git.mycrm.local/api/v1/user +``` diff --git a/docs/PLUGIN_SYSTEM_README.md b/docs/PLUGIN_SYSTEM_README.md index 281c439..51ccfa7 100644 --- a/docs/PLUGIN_SYSTEM_README.md +++ b/docs/PLUGIN_SYSTEM_README.md @@ -10,7 +10,23 @@ Dieses Plugin-System ermöglicht die Installation und Verwaltung von **optionale |-------------|--------|--------| | **Leicht nachträglich installierbar** | ✅ | Composer-basierte Installation ohne Core-Änderungen | | **Keine Core-Abhängigkeiten** | ✅ | Interface-basierte Architektur, lose Kopplung | -| **Lizenzvalidierung erforderlich** | ✅ | REST-API Validierung mit Offline-Fallback (7 Tage Grace Period) | +| **Lizenzvalidierung erforderlich** | ✅ | Zwei Backends: REST-API oder **Gitea Repository-Access** (mit Offline-Fallback) | + +### 🔐 Lizenzierungs-Backends + +Das System unterstützt **zwei verschiedene Lizenzierungs-Backends**: + +1. **Standard-Lizenzserver** (`LicenseValidator`) + - REST-API basiert + - JWT-Tokens als Lizenzen + - Eigener Lizenzserver erforderlich + +2. **Gitea-Lizenzserver** (`GiteaLicenseValidator`) ⭐ **NEU** + - Nutzt Gitea Repository-Zugriff als Lizenzierung + - Gitea Access Token = Lizenzschlüssel + - Keine separate Lizenzserver-API nötig + - Ideal wenn du bereits Gitea nutzt + - **Siehe:** `GITEA_LICENSE_SYSTEM.md` ## 📁 Neu erstellte Dateien @@ -26,7 +42,8 @@ Dieses Plugin-System ermöglicht die Installation und Verwaltung von **optionale │ │ └── ModuleRegistry.php # ⭐ Plugin-Registry mit Auto-Discovery │ │ │ ├── Service/ -│ │ └── LicenseValidator.php # ⭐ Lizenzvalidierung (Online + Cache) +│ │ ├── LicenseValidator.php # ⭐ Standard Lizenzvalidierung (REST-API) +│ │ └── GiteaLicenseValidator.php # ⭐ Gitea-basierte Lizenzvalidierung │ │ │ ├── EventListener/ │ │ └── ModuleBootListener.php # ⭐ Auto-Boot beim Request @@ -47,7 +64,9 @@ Dieses Plugin-System ermöglicht die Installation und Verwaltung von **optionale ├── .env.plugin.example # 📝 Environment-Template │ └── docs/ - ├── PLUGIN_SYSTEM.md # 📖 Vollständige Anleitung + ├── PLUGIN_SYSTEM.md # 📖 Vollständige Anleitung (Standard) + ├── GITEA_LICENSE_SYSTEM.md # 📖 Gitea-Lizenzierung (NEU) + ├── GITEA_QUICKSTART.md # 🚀 Gitea 5-Min Setup ├── PLUGIN_SYSTEM_SUMMARY.md # 📊 Architektur-Zusammenfassung ├── PLUGIN_QUICKSTART.md # 🚀 5-Minuten-Schnellstart ├── EXAMPLE_MODULE_STRUCTURE.md # 📦 Modul-Template-Übersicht diff --git a/src/Command/ModuleLicenseCommand.php b/src/Command/ModuleLicenseCommand.php index 3719a2b..4a0fca7 100644 --- a/src/Command/ModuleLicenseCommand.php +++ b/src/Command/ModuleLicenseCommand.php @@ -35,7 +35,7 @@ class ModuleLicenseCommand extends Command ->addArgument('module', InputArgument::REQUIRED, 'Modul-Identifier') ->addArgument('license-key', InputArgument::OPTIONAL, 'Lizenzschlüssel') ->addOption('revoke', 'r', InputOption::VALUE_NONE, 'Lizenz widerrufen') - ->addOption('validate', 'v', InputOption::VALUE_NONE, 'Lizenz validieren (Force-Refresh)') + ->addOption('validate', null, InputOption::VALUE_NONE, 'Lizenz validieren (Force-Refresh)') ; } @@ -48,11 +48,14 @@ class ModuleLicenseCommand extends Command $revoke = $input->getOption('revoke'); $validate = $input->getOption('validate'); - // Modul prüfen + // Modul prüfen (optional - funktioniert auch ohne installiertes Modul) $module = $this->moduleRegistry->getModule($moduleIdentifier); + if (!$module) { - $io->error(sprintf('Modul "%s" nicht gefunden.', $moduleIdentifier)); - return Command::FAILURE; + $io->note(sprintf( + 'Hinweis: Modul "%s" ist nicht als Composer-Package installiert. Lizenz wird trotzdem registriert.', + $moduleIdentifier + )); } // Lizenz widerrufen @@ -62,19 +65,24 @@ class ModuleLicenseCommand extends Command // Lizenz validieren if ($validate) { - return $this->validateLicense($io, $module); + if ($module) { + return $this->validateLicense($io, $module); + } else { + // Fallback: Direktvalidierung ohne Modul + return $this->validateLicenseDirect($io, $moduleIdentifier); + } } // Lizenz registrieren if ($licenseKey) { - return $this->registerLicense($io, $moduleIdentifier, $licenseKey); + return $this->registerLicense($io, $moduleIdentifier, $licenseKey, $module); } // Interaktive Eingabe return $this->interactiveRegister($io, $moduleIdentifier); } - private function registerLicense(SymfonyStyle $io, string $moduleIdentifier, string $licenseKey): int + private function registerLicense(SymfonyStyle $io, string $moduleIdentifier, string $licenseKey, $module = null): int { $io->section(sprintf('Registriere Lizenz für Modul "%s"', $moduleIdentifier)); @@ -88,9 +96,12 @@ class ModuleLicenseCommand extends Command $io->success('Lizenz erfolgreich registriert!'); // Lizenz-Info anzeigen - $module = $this->moduleRegistry->getModule($moduleIdentifier); if ($module) { $this->displayLicenseInfo($io, $module->getLicenseInfo()); + } else { + // Fallback: Direkte Validierung ohne Modul + $licenseInfo = $this->licenseValidator->validate($moduleIdentifier, $licenseKey); + $this->displayLicenseInfo($io, $licenseInfo); } $io->note('Bitte leeren Sie den Cache, damit das Modul aktiviert wird: php bin/console cache:clear'); @@ -132,23 +143,37 @@ class ModuleLicenseCommand extends Command return Command::SUCCESS; } + private function validateLicenseDirect(SymfonyStyle $io, string $moduleIdentifier): int + { + $io->section(sprintf('Validiere Lizenz für Modul "%s"', $moduleIdentifier)); + + // Direkte Validierung über LicenseValidator + $licenseInfo = $this->licenseValidator->validate($moduleIdentifier); + + if (!$licenseInfo['valid']) { + $io->error(sprintf('Lizenz ungültig: %s', $licenseInfo['message'] ?? 'Unbekannter Fehler')); + return Command::FAILURE; + } + + $io->success('Lizenz gültig!'); + $this->displayLicenseInfo($io, $licenseInfo); + + return Command::SUCCESS; + } + private function interactiveRegister(SymfonyStyle $io, string $moduleIdentifier): int { $io->title(sprintf('Lizenzregistrierung für Modul "%s"', $moduleIdentifier)); - $helper = $this->getHelper('question'); - $question = new Question('Bitte geben Sie den Lizenzschlüssel ein: '); - $question->setHidden(true); - $question->setHiddenFallback(false); - - $licenseKey = $helper->ask($input ?? null, $output ?? null, $question); + $licenseKey = $io->askHidden('Bitte geben Sie den Lizenzschlüssel ein'); if (!$licenseKey) { $io->error('Kein Lizenzschlüssel angegeben.'); return Command::FAILURE; } - return $this->registerLicense($io, $moduleIdentifier, $licenseKey); + $module = $this->moduleRegistry->getModule($moduleIdentifier); + return $this->registerLicense($io, $moduleIdentifier, $licenseKey, $module); } private function displayLicenseInfo(SymfonyStyle $io, array $licenseInfo): void diff --git a/src/Command/RegisterLicenseCommand.php b/src/Command/RegisterLicenseCommand.php new file mode 100644 index 0000000..0638c04 --- /dev/null +++ b/src/Command/RegisterLicenseCommand.php @@ -0,0 +1,125 @@ +addArgument('module-id', InputArgument::REQUIRED, 'Modul-Identifier (z.B. "test", "billing")') + ->addArgument('token', InputArgument::REQUIRED, 'Gitea Access Token') + ->setHelp(<<<'HELP' +Registriert eine Lizenz für ein Modul. + +Das Modul muss NICHT installiert sein - dieser Command registriert nur die Lizenz +in .env.local, damit sie bei zukünftigen Validierungen verwendet wird. + +Beispiel: + php bin/console app:license:register test abc123def456... + +Gitea Token generieren: + Gitea → Settings → Applications → Generate Token (Scope: repo) + +Die Lizenz wird gespeichert als: + GITEA_TOKEN_TEST=abc123def456... +HELP + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $moduleId = $input->getArgument('module-id'); + $token = $input->getArgument('token'); + + $io->title(sprintf('Lizenz-Registrierung für "%s"', $moduleId)); + + // Schritt 1: Validierung durchführen + $io->section('1. Validiere Token...'); + + try { + $result = $this->licenseValidator->validate($moduleId, $token); + + if (!$result['valid']) { + $io->error('Token ungültig!'); + $io->writeln(sprintf('Grund: %s', $result['message'] ?? 'Unbekannt')); + return Command::FAILURE; + } + + $io->success('✓ Token gültig!'); + + // Details anzeigen + $rows = [ + ['Status', '✓ Gültig'], + ]; + + if (isset($result['licensedTo'])) { + $rows[] = ['Lizenziert an', $result['licensedTo']]; + } + + if (isset($result['message'])) { + $rows[] = ['Repository', $result['message']]; + } + + if (isset($result['metadata']['repository_url'])) { + $rows[] = ['URL', $result['metadata']['repository_url']]; + } + + $io->table(['Eigenschaft', 'Wert'], $rows); + + } catch (\Throwable $e) { + $io->error('Validierung fehlgeschlagen'); + $io->writeln($e->getMessage()); + return Command::FAILURE; + } + + // Schritt 2: In .env.local speichern + $io->section('2. Speichere Token in .env.local...'); + + $success = $this->licenseValidator->registerLicense($moduleId, $token); + + if (!$success) { + $io->error('Speichern fehlgeschlagen'); + return Command::FAILURE; + } + + $io->success('✓ Token gespeichert!'); + + $envKey = 'GITEA_TOKEN_' . strtoupper($moduleId); + $io->writeln(sprintf('Gespeichert als: %s', $envKey)); + + // Schritt 3: Cache leeren empfehlen + $io->section('3. Nächste Schritte'); + $io->listing([ + 'Cache leeren: php bin/console cache:clear', + sprintf('Testen: php bin/console app:test:gitea-license %s', $moduleId), + 'Oder: Modul als Composer-Package installieren', + ]); + + return Command::SUCCESS; + } +} diff --git a/src/Command/TestGiteaLicenseCommand.php b/src/Command/TestGiteaLicenseCommand.php new file mode 100644 index 0000000..6bf5b56 --- /dev/null +++ b/src/Command/TestGiteaLicenseCommand.php @@ -0,0 +1,110 @@ +addArgument('module-id', InputArgument::REQUIRED, 'Modul-Identifier (z.B. "test")') + ->addArgument('token', InputArgument::OPTIONAL, 'Gitea Access Token (optional, nutzt ENV)') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $moduleId = $input->getArgument('module-id'); + $token = $input->getArgument('token'); + + $io->title('Gitea-Lizenzvalidierung Test'); + $io->info(sprintf('Teste Modul: %s', $moduleId)); + + if ($token) { + $io->info('Verwende übergebenen Token'); + } else { + $io->info(sprintf('Verwende Token aus ENV: GITEA_TOKEN_%s', strtoupper($moduleId))); + } + + // Validierung durchführen + try { + $result = $this->licenseValidator->validate($moduleId, $token); + + if ($result['valid']) { + $io->success('✓ Lizenz gültig!'); + + // Details anzeigen + $rows = [ + ['Status', '✓ Gültig'], + ]; + + if (isset($result['licensedTo'])) { + $rows[] = ['Lizenziert an', $result['licensedTo']]; + } + + if (isset($result['message'])) { + $rows[] = ['Nachricht', $result['message']]; + } + + if (isset($result['expiresAt']) && $result['expiresAt'] instanceof \DateTimeInterface) { + $rows[] = ['Läuft ab', $result['expiresAt']->format('d.m.Y H:i:s')]; + } + + if (!empty($result['features'])) { + $rows[] = ['Features', implode(', ', $result['features'])]; + } + + if (isset($result['metadata'])) { + foreach ($result['metadata'] as $key => $value) { + if (is_string($value)) { + $rows[] = [ucfirst($key), $value]; + } + } + } + + $io->table(['Eigenschaft', 'Wert'], $rows); + + return Command::SUCCESS; + } else { + $io->error('✗ Lizenz ungültig!'); + $io->writeln(sprintf('Grund: %s', $result['message'] ?? 'Unbekannt')); + + return Command::FAILURE; + } + + } catch (\Throwable $e) { + $io->error('Fehler bei der Validierung'); + $io->writeln($e->getMessage()); + + if ($output->isVerbose()) { + $io->section('Stack Trace'); + $io->writeln($e->getTraceAsString()); + } + + return Command::FAILURE; + } + } +} diff --git a/src/Service/GiteaLicenseValidator.php b/src/Service/GiteaLicenseValidator.php new file mode 100644 index 0000000..9c97b8b --- /dev/null +++ b/src/Service/GiteaLicenseValidator.php @@ -0,0 +1,397 @@ +getStoredLicenseKey($moduleIdentifier); + } + + if (!$licenseKey) { + return $this->createInvalidResponse('Kein Gitea Access Token vorhanden'); + } + + // Cache prüfen + $cacheKey = $this->getCacheKey($moduleIdentifier); + $cachedItem = $this->cache->getItem($cacheKey); + + if ($cachedItem->isHit()) { + $cachedData = $cachedItem->get(); + + // Prüfen, ob Grace Period noch gültig ist + if ($this->isInGracePeriod($cachedData)) { + $this->logger->debug(sprintf( + 'Lizenz für Modul "%s" aus Cache geladen (Grace Period aktiv)', + $moduleIdentifier + )); + return $cachedData; + } + } + + // Online-Validierung durchführen: Gitea Repository Access prüfen + try { + // 1. Repository-Informationen abrufen + $repoName = $this->getRepositoryName($moduleIdentifier); + $repoUrl = sprintf( + '%s/api/v1/repos/%s/%s', + rtrim($this->giteaBaseUrl, '/'), + $this->giteaOrganization, + $repoName + ); + + $response = $this->httpClient->request('GET', $repoUrl, [ + 'headers' => [ + 'Authorization' => 'token ' . $licenseKey, + 'Accept' => 'application/json', + ], + 'timeout' => 10, + ]); + + $statusCode = $response->getStatusCode(); + + if ($statusCode !== 200) { + $this->logger->warning(sprintf( + 'Kein Zugriff auf Gitea-Repository "%s" (Status %d)', + $repoName, + $statusCode + )); + + return $this->createInvalidResponse('Kein Zugriff auf Modul-Repository'); + } + + $repoData = $response->toArray(); + + // 2. Optional: User-Informationen abrufen für "Licensed To" + // (benötigt read:user Scope, daher optional) + $userData = []; + try { + $userResponse = $this->httpClient->request('GET', + rtrim($this->giteaBaseUrl, '/') . '/api/v1/user', + [ + 'headers' => [ + 'Authorization' => 'token ' . $licenseKey, + 'Accept' => 'application/json', + ], + 'timeout' => 10, + ] + ); + + if ($userResponse->getStatusCode() === 200) { + $userData = $userResponse->toArray(); + } + } catch (\Throwable $e) { + // User-API-Call ist optional, ignorieren wenn fehlgeschlagen + $this->logger->debug('User-API-Call fehlgeschlagen (optional): ' . $e->getMessage()); + } + + // 3. Optional: Repository-Tags/Releases für Features und Ablaufdatum + $features = $this->extractFeaturesFromRepository($repoData); + $expiresAt = $this->extractExpirationFromRepository($repoData); + + // Lizenz gültig + $licensedTo = $userData['full_name'] ?? $userData['username'] ?? null; + if (!$licensedTo && isset($repoData['owner']['full_name'])) { + $licensedTo = $repoData['owner']['full_name']; + } elseif (!$licensedTo && isset($repoData['owner']['username'])) { + $licensedTo = $repoData['owner']['username']; + } else { + $licensedTo = $licensedTo ?? 'Repository Owner'; + } + + $validationResult = [ + 'valid' => true, + 'expiresAt' => $expiresAt, + 'licensedTo' => $licensedTo, + 'features' => $features, + 'message' => sprintf( + 'Gitea Repository-Zugriff bestätigt (%s)', + $repoData['full_name'] ?? $repoName + ), + 'cachedUntil' => new \DateTimeImmutable('+' . self::CACHE_TTL . ' seconds'), + 'validatedAt' => new \DateTimeImmutable(), + 'metadata' => [ + 'repository' => $repoData['full_name'] ?? null, + 'repository_url' => $repoData['html_url'] ?? null, + 'gitea_user' => $userData['username'] ?? ($repoData['owner']['username'] ?? null), + ], + ]; + + // In Cache speichern + $cachedItem->set($validationResult); + $cachedItem->expiresAfter(self::CACHE_TTL); + $this->cache->save($cachedItem); + + $this->logger->info(sprintf( + 'Gitea-Lizenz für Modul "%s" erfolgreich validiert (User: %s)', + $moduleIdentifier, + $userData['username'] ?? 'unbekannt' + )); + + return $validationResult; + + } catch (\Throwable $e) { + // Netzwerkfehler oder Access Denied - Grace Period prüfen + $this->logger->error(sprintf( + 'Fehler bei Gitea-Lizenzvalidierung für Modul "%s": %s', + $moduleIdentifier, + $e->getMessage() + )); + + // Wenn gecachte Lizenz vorhanden und in Grace Period, verwenden + if ($cachedItem->isHit()) { + $cachedData = $cachedItem->get(); + + if ($this->isInGracePeriod($cachedData)) { + $this->logger->info(sprintf( + 'Verwende gecachte Gitea-Lizenz für Modul "%s" (Offline-Modus, Grace Period)', + $moduleIdentifier + )); + + $cachedData['message'] = 'Gitea-Lizenz gecacht (Offline-Modus)'; + return $cachedData; + } + } + + return $this->createInvalidResponse( + 'Gitea nicht erreichbar und keine gecachte Lizenz verfügbar' + ); + } + } + + public function registerLicense(string $moduleIdentifier, string $licenseKey): bool + { + try { + // Gitea Token validieren + $validationResult = $this->validate($moduleIdentifier, $licenseKey); + + if (!$validationResult['valid']) { + return false; + } + + // In .env.local speichern + $envFile = dirname(__DIR__, 2) . '/.env.local'; + $envKey = 'GITEA_TOKEN_' . strtoupper($moduleIdentifier); + + $content = file_exists($envFile) ? file_get_contents($envFile) : ''; + $pattern = '/^' . preg_quote($envKey, '/') . '=.*$/m'; + + if (preg_match($pattern, $content)) { + // Bestehenden Eintrag ersetzen + $content = preg_replace($pattern, "$envKey=$licenseKey", $content); + } else { + // Neuen Eintrag hinzufügen + $content .= "\n$envKey=$licenseKey\n"; + } + + file_put_contents($envFile, $content); + + $this->logger->info(sprintf( + 'Gitea-Token für Modul "%s" registriert', + $moduleIdentifier + )); + + return true; + + } catch (\Throwable $e) { + $this->logger->error(sprintf( + 'Fehler beim Registrieren des Gitea-Tokens für Modul "%s": %s', + $moduleIdentifier, + $e->getMessage() + )); + + return false; + } + } + + public function revokeLicense(string $moduleIdentifier): void + { + // Aus Cache löschen + $cacheKey = $this->getCacheKey($moduleIdentifier); + $this->cache->deleteItem($cacheKey); + + // Aus Environment entfernen + $envFile = dirname(__DIR__, 2) . '/.env.local'; + $envKey = 'GITEA_TOKEN_' . strtoupper($moduleIdentifier); + + if (file_exists($envFile)) { + $content = file_get_contents($envFile); + $pattern = '/^' . preg_quote($envKey, '/') . '=.*$\n?/m'; + $content = preg_replace($pattern, '', $content); + file_put_contents($envFile, $content); + } + + $this->logger->info(sprintf( + 'Gitea-Token für Modul "%s" widerrufen', + $moduleIdentifier + )); + } + + public function getRegisteredLicenses(): array + { + $licenses = []; + + // Alle GITEA_TOKEN_* Environment-Variablen durchsuchen + foreach ($_ENV as $key => $value) { + if (str_starts_with($key, 'GITEA_TOKEN_')) { + $moduleIdentifier = strtolower(substr($key, 12)); + $licenses[$moduleIdentifier] = $this->validate($moduleIdentifier, $value); + } + } + + return $licenses; + } + + /** + * Holt einen gespeicherten Gitea Access Token + */ + private function getStoredLicenseKey(string $moduleIdentifier): ?string + { + $envKey = 'GITEA_TOKEN_' . strtoupper($moduleIdentifier); + return $_ENV[$envKey] ?? null; + } + + /** + * Generiert Repository-Namen aus Modul-Identifier + * + * Beispiel: "billing" -> "mycrm-billing-module" + */ + private function getRepositoryName(string $moduleIdentifier): string + { + return 'mycrm-' . $moduleIdentifier . '-module'; + } + + /** + * Extrahiert Features aus Repository-Metadaten + * + * Features können in Repository-Topics oder Tags definiert werden + */ + private function extractFeaturesFromRepository(array $repoData): array + { + $features = []; + + // Features aus Topics (Gitea Repository Topics) + if (isset($repoData['topics']) && is_array($repoData['topics'])) { + foreach ($repoData['topics'] as $topic) { + if (str_starts_with($topic, 'feature-')) { + $features[] = substr($topic, 8); // "feature-" entfernen + } + } + } + + // Fallback: Alle Topics als Features + if (empty($features) && isset($repoData['topics'])) { + $features = $repoData['topics']; + } + + return $features; + } + + /** + * Extrahiert Ablaufdatum aus Repository-Metadaten + * + * Kann z.B. aus Repository-Description geparst werden: + * "License expires: 2025-12-31" + */ + private function extractExpirationFromRepository(array $repoData): ?\DateTimeImmutable + { + if (!isset($repoData['description'])) { + return null; + } + + $description = $repoData['description']; + + // Pattern: "expires: YYYY-MM-DD" oder "valid until: YYYY-MM-DD" + if (preg_match('/(?:expires?|valid until):\s*(\d{4}-\d{2}-\d{2})/i', $description, $matches)) { + try { + return new \DateTimeImmutable($matches[1]); + } catch (\Exception $e) { + $this->logger->warning(sprintf( + 'Ungültiges Ablaufdatum in Repository-Description: %s', + $matches[1] + )); + } + } + + return null; + } + + /** + * Prüft, ob eine gecachte Lizenz noch in der Grace Period ist + */ + private function isInGracePeriod(array $cachedData): bool + { + if (!isset($cachedData['validatedAt']) || !$cachedData['valid']) { + return false; + } + + $validatedAt = $cachedData['validatedAt']; + if (!$validatedAt instanceof \DateTimeInterface) { + return false; + } + + $now = new \DateTimeImmutable(); + $gracePeriodEnd = $validatedAt->modify('+' . self::GRACE_PERIOD . ' seconds'); + + return $now <= $gracePeriodEnd; + } + + /** + * Erstellt eine "ungültig" Response + */ + private function createInvalidResponse(string $message): array + { + return [ + 'valid' => false, + 'expiresAt' => null, + 'licensedTo' => null, + 'features' => [], + 'message' => $message, + 'cachedUntil' => null, + ]; + } + + /** + * Generiert Cache-Key für ein Modul + */ + private function getCacheKey(string $moduleIdentifier): string + { + return self::CACHE_PREFIX . $moduleIdentifier; + } +} diff --git a/symfony.lock b/symfony.lock index 3f51737..aee5f57 100644 --- a/symfony.lock +++ b/symfony.lock @@ -73,6 +73,9 @@ "config/packages/knpu_oauth2_client.yaml" ] }, + "mycrm/test-module": { + "version": "dev-main" + }, "nelmio/cors-bundle": { "version": "2.6", "recipe": {