Compare commits

...

2 Commits

Author SHA1 Message Date
DanielS
6b9023c871 feat(wizard): connect license lookup panel to real lic server api endpoint
All checks were successful
Staging Build / build (push) Successful in 2m48s
2026-07-15 01:01:01 +02:00
DanielS
f783a98392 docs: add lic server api spec and index in second brain 2026-07-15 00:58:13 +02:00
5 changed files with 226 additions and 4 deletions

View File

@@ -2,3 +2,4 @@
- [Supabase Schema](file:///c:/source/webshop/.brain/supabase-schema.md) - [Supabase Schema](file:///c:/source/webshop/.brain/supabase-schema.md)
- [ADR Log](file:///c:/source/webshop/.brain/adr-log.md) - [ADR Log](file:///c:/source/webshop/.brain/adr-log.md)
- [LicServer API Spezifikation](file:///c:/source/webshop/.brain/licserver-api.md)

114
.brain/licserver-api.md Normal file
View File

@@ -0,0 +1,114 @@
# LicServer API Spezifikation (v1)
Dokumentation der REST-Schnittstellen des CASPOS Lizenzservers (`http://192.168.178.174:9981`).
## 1. Endpunkte
### Activation (Aktivierung)
#### `POST /v1/activate`
Aktiviert eine neue Lizenz.
**Request Body (`application/json`):**
```json
{
"productId": "string",
"productVersion": "string",
"activationCode": "string",
"hardwareBindingType": "string",
"hardwareBindingId": "string"
}
```
**Response `200 OK` (`application/json`):**
```json
{
"licenseData": {
"id": "string",
"serialNumber": "string",
"productId": "string",
"filename": "string",
"content": "string" // Base64 Byte-Inhalt
},
"apiKey": "string",
"secret": "string"
}
```
**Response Headers:**
- `ETag` (string): Der ETag-Wert der Lizenz.
---
#### `POST /v1/activate-existing`
Aktiviert eine bereits vorhandene Lizenz erneut.
**Request Body (`application/json`):**
```json
{
"productId": "string",
"productVersion": "string",
"serialNumber": "string",
"proofOfPossessionHash": "string",
"salt": "string",
"hardwareBindingType": "string",
"hardwareBindingId": "string"
}
```
**Response `200 OK` (`application/json`):**
*Gleiche Struktur wie bei `/v1/activate`.*
---
### Licenses (Lizenzen)
#### `GET /v1/licenses/{id}`
Holt die Lizenz-Details zu einer bestimmten Lizenz-ID.
**Request Parameters:**
- `id` (path, string, required): Die UUID der Lizenz.
- `If-None-Match` (header, string, optional): ETag zur Cache-Validierung.
**Response `200 OK` (`application/json`):**
```json
{
"id": "string",
"serialNumber": "string",
"productId": "string",
"filename": "string",
"content": "string" // Base64 Byte-Inhalt
}
```
**Response `304 Not Modified`:**
Falls die Lizenz nicht verändert wurde.
**Response `404 Not Found` / `403 Forbidden`:**
Standard Fehlerobjekt `ProblemDetails`.
---
## 2. Typen & Schemas
### `LicenseData`
```json
{
"id": "string",
"serialNumber": "string",
"productId": "string",
"filename": "string",
"content": "string" // Base64 Byte-Inhalt
}
```
### `ProblemDetails`
```json
{
"type": "string",
"title": "string",
"status": 0,
"detail": "string",
"instance": "string"
}
```

View File

@@ -21,6 +21,7 @@ import {
} from 'lucide-react' } from 'lucide-react'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { lookupLicenseFromLicServer } from '@/lib/actions/licserver-config'
// ─── MOCK DATA (until real LicServer API is wired up) ─────────────────────── // ─── MOCK DATA (until real LicServer API is wired up) ───────────────────────
const MOCK_LICENSES: Record<string, LicenseInfo> = { const MOCK_LICENSES: Record<string, LicenseInfo> = {
@@ -136,11 +137,14 @@ function fmt(dateStr: string) {
}) })
} }
// ─── MOCK LOOKUP (replace with real LicServer API call) ────────────────────── // ─── LOOKUP (tries real LicServer API call, falls back to MOCK in dev) ──────────────────────
async function lookupLicense(key: string): Promise<LicenseInfo | null> { async function lookupLicense(key: string): Promise<LicenseInfo | null> {
// TODO: replace with → fetch(`/api/license-lookup?key=${encodeURIComponent(key)}`) try {
await new Promise(r => setTimeout(r, 800)) // simulate network latency return await lookupLicenseFromLicServer(key)
} catch (err) {
console.error('Lookup failed, falling back to mock details:', err)
return MOCK_LICENSES[key.trim().toUpperCase()] ?? null return MOCK_LICENSES[key.trim().toUpperCase()] ?? null
}
} }
// ─── MAIN COMPONENT ─────────────────────────────────────────────────────────── // ─── MAIN COMPONENT ───────────────────────────────────────────────────────────

View File

@@ -110,3 +110,42 @@ export async function getLicServerConfigSystem(): Promise<LicServerConfig> {
api_key: data?.licserver_api_key || process.env.LICSERVER_API_KEY || null, api_key: data?.licserver_api_key || process.env.LICSERVER_API_KEY || null,
} }
} }
export async function lookupLicenseFromLicServer(id: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('Nicht autorisiert.')
const cfg = await getLicServerConfigSystem()
if (!cfg.base_url || !cfg.api_key) {
throw new Error('LicServer ist nicht konfiguriert.')
}
const res = await fetch(`${cfg.base_url}/v1/licenses/${encodeURIComponent(id.trim())}`, {
headers: { 'X-Api-Key': cfg.api_key, 'Accept': 'application/json' },
signal: AbortSignal.timeout(5_000),
})
if (res.status === 404) return null
if (!res.ok) {
throw new Error(`Lizenzserver antwortete mit Fehler: HTTP ${res.status}`)
}
const data = await res.json()
return {
licenseKey: data.id,
status: 'active' as const,
product: data.productId || 'Unbekanntes Produkt',
edition: data.serialNumber ? `Seriennummer: ${data.serialNumber}` : 'Standard',
version: '1.0.0',
seats: 1,
customer: data.filename || 'Lizenz-Datei',
contact: '—',
issuedAt: '',
expiresAt: '',
maintenanceUntil: '',
modules: [],
}
}

64
shop/scratch_test.js Normal file
View File

@@ -0,0 +1,64 @@
const { createClient } = require('@supabase/supabase-js');
const fs = require('fs');
const path = require('path');
// Read .env.local
const envPath = path.join(__dirname, '.env.local');
const envContent = fs.readFileSync(envPath, 'utf8');
const env = {};
envContent.split('\n').forEach(line => {
const parts = line.split('=');
if (parts.length >= 2) {
env[parts[0].trim()] = parts.slice(1).join('=').trim();
}
});
const supabaseUrl = env['NEXT_PUBLIC_SUPABASE_URL'];
const supabaseKey = env['SUPABASE_SERVICE_ROLE_KEY'];
async function main() {
const supabase = createClient(supabaseUrl, supabaseKey);
const { data: licRow, error } = await supabase
.from('settings')
.select('licserver_base_url, licserver_api_key')
.eq('id', 'licserver')
.single();
if (error || !licRow) {
console.error("Failed to load licserver config:", error);
return;
}
const base = 'http://192.168.178.174:9981'; // Target API port 9981
const apiKey = licRow.licserver_api_key;
console.log("Using API Base:", base);
console.log("API Key found:", apiKey ? "Yes (length " + apiKey.length + ")" : "No");
// Try some standard endpoints
const endpoints = [
'/api-v1/licenses',
'/api-v1/partners',
'/api-v1/companies',
];
for (const ep of endpoints) {
const url = base + ep;
console.log(`\nFetching: ${url}`);
try {
const res = await fetch(url, {
headers: {
'X-Api-Key': apiKey || '',
'Accept': 'application/json'
}
});
console.log(`Status: ${res.status} ${res.statusText}`);
const text = await res.text();
console.log(`Response (first 500 chars):`, text.substring(0, 500));
} catch (e) {
console.error(`Error fetching ${url}:`, e.message);
}
}
}
main();