Update License and P System

This commit is contained in:
olli 2025-12-04 10:53:18 +01:00
parent 42e7bc7e10
commit 7d6ef9f0eb
16 changed files with 1538 additions and 26 deletions

9
.env
View File

@ -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 ###

View File

@ -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 ###

1
.gitignore vendored
View File

@ -25,3 +25,4 @@
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###
auth.json

View File

@ -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"
}
}
}

38
composer.lock generated
View File

@ -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",

View File

@ -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],
];

View File

@ -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

View File

@ -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:

View File

@ -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 <module-identifier> <gitea-token>
# 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

238
docs/GITEA_QUICKSTART.md Normal file
View File

@ -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
```

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,125 @@
<?php
namespace App\Command;
use App\Plugin\LicenseValidatorInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Registriert Lizenzen direkt im LicenseValidator
* Benötigt KEIN installiertes Modul
*/
#[AsCommand(
name: 'app:license:register',
description: 'Registriert eine Lizenz (ohne installiertes Modul)',
)]
class RegisterLicenseCommand extends Command
{
public function __construct(
private readonly LicenseValidatorInterface $licenseValidator
) {
parent::__construct();
}
protected function configure(): void
{
$this
->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: <info>%s</info>', $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;
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace App\Command;
use App\Plugin\LicenseValidatorInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Test-Command für Gitea-Lizenzvalidierung
*/
#[AsCommand(
name: 'app:test:gitea-license',
description: 'Testet die Gitea-Lizenzvalidierung direkt',
)]
class TestGiteaLicenseCommand extends Command
{
public function __construct(
private readonly LicenseValidatorInterface $licenseValidator
) {
parent::__construct();
}
protected function configure(): void
{
$this
->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;
}
}
}

View File

@ -0,0 +1,397 @@
<?php
namespace App\Service;
use App\Plugin\LicenseValidatorInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* Gitea-basierte Lizenzvalidierung
*
* Nutzt Gitea Repository Access als Lizenzierung:
* - Zugriff auf privates Modul-Repository = gültige Lizenz
* - Gitea Access Token = Lizenzschlüssel
* - Repository-Metadaten = Lizenzinformationen (Tags, Releases)
*
* Vorteile:
* - Keine separate Lizenzserver-API nötig
* - Nutzt bestehende Gitea-Infrastruktur
* - Access Control über Gitea Repository-Permissions
* - Einfache Verwaltung über Gitea UI
*/
class GiteaLicenseValidator implements LicenseValidatorInterface
{
private const CACHE_PREFIX = 'gitea_license_';
private const CACHE_TTL = 86400; // 24 Stunden
private const GRACE_PERIOD = 604800; // 7 Tage
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly CacheItemPoolInterface $cache,
private readonly LoggerInterface $logger,
private readonly string $giteaBaseUrl,
private readonly string $giteaOrganization,
private readonly string $instanceId
) {
}
public function validate(string $moduleIdentifier, ?string $licenseKey = null): array
{
// Gitea Access Token als Lizenzschlüssel
if (!$licenseKey) {
$licenseKey = $this->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;
}
}

View File

@ -73,6 +73,9 @@
"config/packages/knpu_oauth2_client.yaml"
]
},
"mycrm/test-module": {
"version": "dev-main"
},
"nelmio/cors-bundle": {
"version": "2.6",
"recipe": {