Update License and P System
This commit is contained in:
parent
42e7bc7e10
commit
7d6ef9f0eb
9
.env
9
.env
@ -72,3 +72,12 @@ GITHUB_TOKEN=
|
|||||||
# Optional: Gitea Access Token for private instances
|
# Optional: Gitea Access Token for private instances
|
||||||
GITEA_TOKEN=
|
GITEA_TOKEN=
|
||||||
###< app/git-services ###
|
###< 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 ###
|
||||||
|
|||||||
@ -3,9 +3,26 @@
|
|||||||
|
|
||||||
###> Plugin-System ###
|
###> 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)
|
# URL des Lizenzservers (ohne trailing slash)
|
||||||
LICENSE_SERVER_URL=https://license.mycrm.local
|
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
|
# Eindeutige Instance-ID (generiere mit: php bin/console app:generate-instance-id
|
||||||
# oder verwende: uuidgen / openssl rand -hex 16)
|
# oder verwende: uuidgen / openssl rand -hex 16)
|
||||||
INSTANCE_ID=your-unique-instance-identifier-here
|
INSTANCE_ID=your-unique-instance-identifier-here
|
||||||
@ -13,7 +30,9 @@ INSTANCE_ID=your-unique-instance-identifier-here
|
|||||||
###< Plugin-System ###
|
###< Plugin-System ###
|
||||||
|
|
||||||
###> Modul-Lizenzen ###
|
###> 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
|
# Beispiel: Billing-Modul
|
||||||
# LICENSE_BILLING=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
# LICENSE_BILLING=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
@ -21,8 +40,18 @@ INSTANCE_ID=your-unique-instance-identifier-here
|
|||||||
# Beispiel: Invoicing-Modul
|
# Beispiel: Invoicing-Modul
|
||||||
# LICENSE_INVOICING=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
# LICENSE_INVOICING=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
|
||||||
# Beispiel: Inventory-Modul
|
# === Für Gitea-Lizenzserver (LICENSE_BACKEND=GiteaLicenseValidator) ===
|
||||||
# LICENSE_INVENTORY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
# 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 ###
|
###< Modul-Lizenzen ###
|
||||||
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,3 +25,4 @@
|
|||||||
npm-debug.log
|
npm-debug.log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
###< symfony/webpack-encore-bundle ###
|
###< symfony/webpack-encore-bundle ###
|
||||||
|
auth.json
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"doctrine/orm": "^3.5",
|
"doctrine/orm": "^3.5",
|
||||||
"knpuniversity/oauth2-client-bundle": "*",
|
"knpuniversity/oauth2-client-bundle": "*",
|
||||||
"league/oauth2-client": "*",
|
"league/oauth2-client": "*",
|
||||||
|
"mycrm/test-module": "*",
|
||||||
"nelmio/cors-bundle": "^2.6",
|
"nelmio/cors-bundle": "^2.6",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
"phpstan/phpdoc-parser": "^2.3",
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
@ -112,5 +113,11 @@
|
|||||||
"symfony/maker-bundle": "^1.0",
|
"symfony/maker-bundle": "^1.0",
|
||||||
"symfony/stopwatch": "7.1.*",
|
"symfony/stopwatch": "7.1.*",
|
||||||
"symfony/web-profiler-bundle": "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
38
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "c70cc2a152b707d0dcf4c562bd06f8d5",
|
"content-hash": "caa943b57e0e058e754382b31531775b",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "api-platform/core",
|
"name": "api-platform/core",
|
||||||
@ -2038,6 +2038,42 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-03-24T10:02:05+00:00"
|
"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",
|
"name": "nelmio/cors-bundle",
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
|
|||||||
@ -18,4 +18,5 @@ return [
|
|||||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||||
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||||
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
||||||
|
MyCRM\TestModule\TestModuleBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@ -14,6 +14,11 @@ services:
|
|||||||
_defaults:
|
_defaults:
|
||||||
autowire: true # Automatically injects dependencies in your services.
|
autowire: true # Automatically injects dependencies in your services.
|
||||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
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
|
# makes classes in src/ available to be used as services
|
||||||
# this creates a service per class whose id is the fully-qualified class name
|
# this creates a service per class whose id is the fully-qualified class name
|
||||||
|
|||||||
@ -1,13 +1,20 @@
|
|||||||
# Plugin-System Services Configuration
|
# Plugin-System Services Configuration
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# Lizenz-Validator
|
_defaults:
|
||||||
App\Plugin\LicenseValidatorInterface:
|
autowire: true
|
||||||
class: App\Service\LicenseValidator
|
autoconfigure: true
|
||||||
arguments:
|
bind:
|
||||||
$licenseServerUrl: '%env(LICENSE_SERVER_URL)%'
|
$licenseServerUrl: '%env(LICENSE_SERVER_URL)%'
|
||||||
|
$giteaBaseUrl: '%env(GITEA_BASE_URL)%'
|
||||||
|
$giteaOrganization: '%env(GITEA_ORGANIZATION)%'
|
||||||
$instanceId: '%env(INSTANCE_ID)%'
|
$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)
|
# Module Registry (automatisches TaggedIterator sammelt alle Plugins)
|
||||||
App\Plugin\ModuleRegistry:
|
App\Plugin\ModuleRegistry:
|
||||||
arguments:
|
arguments:
|
||||||
|
|||||||
500
docs/GITEA_LICENSE_SYSTEM.md
Normal file
500
docs/GITEA_LICENSE_SYSTEM.md
Normal 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
238
docs/GITEA_QUICKSTART.md
Normal 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
|
||||||
|
```
|
||||||
@ -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 |
|
| **Leicht nachträglich installierbar** | ✅ | Composer-basierte Installation ohne Core-Änderungen |
|
||||||
| **Keine Core-Abhängigkeiten** | ✅ | Interface-basierte Architektur, lose Kopplung |
|
| **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
|
## 📁 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
|
│ │ └── ModuleRegistry.php # ⭐ Plugin-Registry mit Auto-Discovery
|
||||||
│ │
|
│ │
|
||||||
│ ├── Service/
|
│ ├── Service/
|
||||||
│ │ └── LicenseValidator.php # ⭐ Lizenzvalidierung (Online + Cache)
|
│ │ ├── LicenseValidator.php # ⭐ Standard Lizenzvalidierung (REST-API)
|
||||||
|
│ │ └── GiteaLicenseValidator.php # ⭐ Gitea-basierte Lizenzvalidierung
|
||||||
│ │
|
│ │
|
||||||
│ ├── EventListener/
|
│ ├── EventListener/
|
||||||
│ │ └── ModuleBootListener.php # ⭐ Auto-Boot beim Request
|
│ │ └── 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
|
├── .env.plugin.example # 📝 Environment-Template
|
||||||
│
|
│
|
||||||
└── docs/
|
└── 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_SYSTEM_SUMMARY.md # 📊 Architektur-Zusammenfassung
|
||||||
├── PLUGIN_QUICKSTART.md # 🚀 5-Minuten-Schnellstart
|
├── PLUGIN_QUICKSTART.md # 🚀 5-Minuten-Schnellstart
|
||||||
├── EXAMPLE_MODULE_STRUCTURE.md # 📦 Modul-Template-Übersicht
|
├── EXAMPLE_MODULE_STRUCTURE.md # 📦 Modul-Template-Übersicht
|
||||||
|
|||||||
@ -35,7 +35,7 @@ class ModuleLicenseCommand extends Command
|
|||||||
->addArgument('module', InputArgument::REQUIRED, 'Modul-Identifier')
|
->addArgument('module', InputArgument::REQUIRED, 'Modul-Identifier')
|
||||||
->addArgument('license-key', InputArgument::OPTIONAL, 'Lizenzschlüssel')
|
->addArgument('license-key', InputArgument::OPTIONAL, 'Lizenzschlüssel')
|
||||||
->addOption('revoke', 'r', InputOption::VALUE_NONE, 'Lizenz widerrufen')
|
->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');
|
$revoke = $input->getOption('revoke');
|
||||||
$validate = $input->getOption('validate');
|
$validate = $input->getOption('validate');
|
||||||
|
|
||||||
// Modul prüfen
|
// Modul prüfen (optional - funktioniert auch ohne installiertes Modul)
|
||||||
$module = $this->moduleRegistry->getModule($moduleIdentifier);
|
$module = $this->moduleRegistry->getModule($moduleIdentifier);
|
||||||
|
|
||||||
if (!$module) {
|
if (!$module) {
|
||||||
$io->error(sprintf('Modul "%s" nicht gefunden.', $moduleIdentifier));
|
$io->note(sprintf(
|
||||||
return Command::FAILURE;
|
'Hinweis: Modul "%s" ist nicht als Composer-Package installiert. Lizenz wird trotzdem registriert.',
|
||||||
|
$moduleIdentifier
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lizenz widerrufen
|
// Lizenz widerrufen
|
||||||
@ -62,19 +65,24 @@ class ModuleLicenseCommand extends Command
|
|||||||
|
|
||||||
// Lizenz validieren
|
// Lizenz validieren
|
||||||
if ($validate) {
|
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
|
// Lizenz registrieren
|
||||||
if ($licenseKey) {
|
if ($licenseKey) {
|
||||||
return $this->registerLicense($io, $moduleIdentifier, $licenseKey);
|
return $this->registerLicense($io, $moduleIdentifier, $licenseKey, $module);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interaktive Eingabe
|
// Interaktive Eingabe
|
||||||
return $this->interactiveRegister($io, $moduleIdentifier);
|
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));
|
$io->section(sprintf('Registriere Lizenz für Modul "%s"', $moduleIdentifier));
|
||||||
|
|
||||||
@ -88,9 +96,12 @@ class ModuleLicenseCommand extends Command
|
|||||||
$io->success('Lizenz erfolgreich registriert!');
|
$io->success('Lizenz erfolgreich registriert!');
|
||||||
|
|
||||||
// Lizenz-Info anzeigen
|
// Lizenz-Info anzeigen
|
||||||
$module = $this->moduleRegistry->getModule($moduleIdentifier);
|
|
||||||
if ($module) {
|
if ($module) {
|
||||||
$this->displayLicenseInfo($io, $module->getLicenseInfo());
|
$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');
|
$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;
|
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
|
private function interactiveRegister(SymfonyStyle $io, string $moduleIdentifier): int
|
||||||
{
|
{
|
||||||
$io->title(sprintf('Lizenzregistrierung für Modul "%s"', $moduleIdentifier));
|
$io->title(sprintf('Lizenzregistrierung für Modul "%s"', $moduleIdentifier));
|
||||||
|
|
||||||
$helper = $this->getHelper('question');
|
$licenseKey = $io->askHidden('Bitte geben Sie den Lizenzschlüssel ein');
|
||||||
$question = new Question('Bitte geben Sie den Lizenzschlüssel ein: ');
|
|
||||||
$question->setHidden(true);
|
|
||||||
$question->setHiddenFallback(false);
|
|
||||||
|
|
||||||
$licenseKey = $helper->ask($input ?? null, $output ?? null, $question);
|
|
||||||
|
|
||||||
if (!$licenseKey) {
|
if (!$licenseKey) {
|
||||||
$io->error('Kein Lizenzschlüssel angegeben.');
|
$io->error('Kein Lizenzschlüssel angegeben.');
|
||||||
return Command::FAILURE;
|
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
|
private function displayLicenseInfo(SymfonyStyle $io, array $licenseInfo): void
|
||||||
|
|||||||
125
src/Command/RegisterLicenseCommand.php
Normal file
125
src/Command/RegisterLicenseCommand.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/Command/TestGiteaLicenseCommand.php
Normal file
110
src/Command/TestGiteaLicenseCommand.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
397
src/Service/GiteaLicenseValidator.php
Normal file
397
src/Service/GiteaLicenseValidator.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -73,6 +73,9 @@
|
|||||||
"config/packages/knpu_oauth2_client.yaml"
|
"config/packages/knpu_oauth2_client.yaml"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"mycrm/test-module": {
|
||||||
|
"version": "dev-main"
|
||||||
|
},
|
||||||
"nelmio/cors-bundle": {
|
"nelmio/cors-bundle": {
|
||||||
"version": "2.6",
|
"version": "2.6",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user