diff --git a/shop/app/admin/einstellungen/page.tsx b/shop/app/admin/einstellungen/page.tsx index a758cc6..b7bf56f 100644 --- a/shop/app/admin/einstellungen/page.tsx +++ b/shop/app/admin/einstellungen/page.tsx @@ -4,8 +4,11 @@ import { useState, useEffect } from 'react'; import { Switch } from '@/components/ui/switch'; import { Button } from '@/components/ui/button'; -import { Download, Upload, Database, AlertTriangle, Loader2 } from 'lucide-react'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Download, Upload, Database, AlertTriangle, Loader2, KeyRound, Server, CheckCircle2, XCircle, Eye, EyeOff, Wifi } from 'lucide-react'; import { createClient } from '@/lib/supabase/client'; +import { saveLicServerConfig, testLicServerConnection } from '@/lib/actions/licserver-config'; import { useRouter } from 'next/navigation'; export default function AdminSettings() { @@ -16,6 +19,16 @@ export default function AdminSettings() { const [statusType, setStatusType] = useState<'success' | 'error' | 'info' | ''>(''); const [selectedFile, setSelectedFile] = useState(null); const [loading, setLoading] = useState(true); + + // LicServer Config State + const [licUrl, setLicUrl] = useState(''); + const [licKey, setLicKey] = useState(''); + const [showKey, setShowKey] = useState(false); + const [licSaving, setLicSaving] = useState(false); + const [licTesting, setLicTesting] = useState(false); + const [licStatus, setLicStatus] = useState<{ ok: boolean; message: string } | null>(null); + const [licMsg, setLicMsg] = useState(''); + const [licMsgType, setLicMsgType] = useState<'success' | 'error' | ''>(''); const router = useRouter(); useEffect(() => { @@ -37,6 +50,18 @@ export default function AdminSettings() { const state = localStorage.getItem('demo_banner_disabled') !== 'true'; setDemoActive(state); setLoading(false); + + // Load current LicServer config from DB + const supabaseClient = createClient(); + const { data: licRow } = await supabaseClient + .from('settings') + .select('licserver_base_url, licserver_api_key') + .eq('id', 'licserver') + .single(); + if (licRow) { + setLicUrl(licRow.licserver_base_url || ''); + setLicKey(licRow.licserver_api_key || ''); + } } } checkAccess(); @@ -232,6 +257,128 @@ export default function AdminSettings() { + {/* LicServer Konfiguration */} +
+
+

+ + LicServer Konfiguration +

+

+ Verbindungseinstellungen zum CASPOS Lizenzserver (internes Container-Netz). + Der API-Key wird verschlüsselt in der Datenbank gespeichert und nie an den Browser übertragen. +

+
+ + {/* Status Message */} + {licMsg && ( +
+ {licMsg} +
+ )} + + {/* Connection Test Result */} + {licStatus && ( +
+ {licStatus.ok + ? + : } + {licStatus.message} +
+ )} + +
+ {/* Base URL */} +
+ + setLicUrl(e.target.value)} + placeholder="http://192.168.178.174:9980" + className="text-sm font-mono" + /> +

Interne Container-URL ohne trailing slash

+
+ + {/* API Key */} +
+ +
+ setLicKey(e.target.value)} + placeholder="Ihr X-Api-Key" + className="text-sm font-mono pr-10" + /> + +
+

Wird als X-Api-Key Header an den LicServer gesendet

+
+
+ + {/* Actions */} +
+ + + +
+
+ ); } diff --git a/shop/app/api/licserver/partners/[id]/route.ts b/shop/app/api/licserver/partners/[id]/route.ts index ecea03e..d6e2d7a 100644 --- a/shop/app/api/licserver/partners/[id]/route.ts +++ b/shop/app/api/licserver/partners/[id]/route.ts @@ -1,10 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' +import { getLicServerConfig } from '@/lib/actions/licserver-config' import type { PartnerDto, LicServerProblemDetails } from '@/lib/types/licserver' -const LICSERVER_BASE_URL = process.env.LICSERVER_BASE_URL -const LICSERVER_API_KEY = process.env.LICSERVER_API_KEY - /** * GET /api/licserver/partners/[id] * @@ -12,6 +10,8 @@ const LICSERVER_API_KEY = process.env.LICSERVER_API_KEY * [id] must be a valid UUID. * * Secured: requires a valid Supabase session. + * base_url + api_key are read from DB settings (admin-configurable), + * with fallback to LICSERVER_BASE_URL / LICSERVER_API_KEY env vars. */ export async function GET( _req: NextRequest, @@ -24,11 +24,12 @@ export async function GET( return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - // ── Env check ────────────────────────────────────────────────────────── - if (!LICSERVER_BASE_URL || !LICSERVER_API_KEY) { - console.error('[licserver] LICSERVER_BASE_URL or LICSERVER_API_KEY not set') + // ── Load config from DB (falls back to env vars) ────────────────────── + const cfg = await getLicServerConfig() + if (!cfg.base_url || !cfg.api_key) { + console.error('[licserver] base_url or api_key not configured (DB or env)') return NextResponse.json( - { error: 'LicServer not configured' }, + { error: 'LicServer nicht konfiguriert. Bitte in den Admin-Einstellungen hinterlegen.' }, { status: 503 } ) } @@ -44,11 +45,11 @@ export async function GET( // ── Proxy request ────────────────────────────────────────────────────── try { const res = await fetch( - `${LICSERVER_BASE_URL}/api-v1/partners/${id}`, + `${cfg.base_url}/api-v1/partners/${id}`, { method: 'GET', headers: { - 'X-Api-Key': LICSERVER_API_KEY, + 'X-Api-Key': cfg.api_key, 'Accept': 'application/json', }, signal: AbortSignal.timeout(5_000), diff --git a/shop/app/api/licserver/partners/route.ts b/shop/app/api/licserver/partners/route.ts index 5190e01..35d99a0 100644 --- a/shop/app/api/licserver/partners/route.ts +++ b/shop/app/api/licserver/partners/route.ts @@ -1,10 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' +import { getLicServerConfig } from '@/lib/actions/licserver-config' import type { PartnerDtoPagedResult, LicServerProblemDetails } from '@/lib/types/licserver' -const LICSERVER_BASE_URL = process.env.LICSERVER_BASE_URL -const LICSERVER_API_KEY = process.env.LICSERVER_API_KEY - /** * GET /api/licserver/partners * @@ -12,7 +10,8 @@ const LICSERVER_API_KEY = process.env.LICSERVER_API_KEY * Query params forwarded: search, page, pageSize * * Secured: requires a valid Supabase session. - * The X-Api-Key is injected server-side and never exposed to the browser. + * base_url + api_key are read from DB settings (admin-configurable), + * with fallback to LICSERVER_BASE_URL / LICSERVER_API_KEY env vars. */ export async function GET(req: NextRequest) { // ── Auth guard ───────────────────────────────────────────────────────── @@ -22,18 +21,19 @@ export async function GET(req: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - // ── Env check ────────────────────────────────────────────────────────── - if (!LICSERVER_BASE_URL || !LICSERVER_API_KEY) { - console.error('[licserver] LICSERVER_BASE_URL or LICSERVER_API_KEY not set') + // ── Load config from DB (falls back to env vars) ────────────────────── + const cfg = await getLicServerConfig() + if (!cfg.base_url || !cfg.api_key) { + console.error('[licserver] base_url or api_key not configured (DB or env)') return NextResponse.json( - { error: 'LicServer not configured' }, + { error: 'LicServer nicht konfiguriert. Bitte in den Admin-Einstellungen hinterlegen.' }, { status: 503 } ) } // ── Forward query params ─────────────────────────────────────────────── const { searchParams } = req.nextUrl - const upstream = new URL(`${LICSERVER_BASE_URL}/api-v1/partners`) + const upstream = new URL(`${cfg.base_url}/api-v1/partners`) const search = searchParams.get('search') const page = searchParams.get('page') const pageSize = searchParams.get('pageSize') @@ -46,7 +46,7 @@ export async function GET(req: NextRequest) { const res = await fetch(upstream.toString(), { method: 'GET', headers: { - 'X-Api-Key': LICSERVER_API_KEY, + 'X-Api-Key': cfg.api_key, 'Accept': 'application/json', }, // Internal container network — short timeout diff --git a/shop/lib/actions/licserver-config.ts b/shop/lib/actions/licserver-config.ts new file mode 100644 index 0000000..22722b2 --- /dev/null +++ b/shop/lib/actions/licserver-config.ts @@ -0,0 +1,86 @@ +'use server' + +import { createClient } from '@/lib/supabase/server' + +export interface LicServerConfig { + base_url: string | null + api_key: string | null +} + +/** + * Read LicServer config from the settings table (admin only). + * Falls back to environment variables if DB values are empty. + */ +export async function getLicServerConfig(): Promise { + const supabase = await createClient() + + const { data } = await supabase + .from('settings') + .select('licserver_base_url, licserver_api_key') + .eq('id', 'licserver') + .single() + + return { + base_url: data?.licserver_base_url || process.env.LICSERVER_BASE_URL || null, + api_key: data?.licserver_api_key || process.env.LICSERVER_API_KEY || null, + } +} + +/** + * Save LicServer config. Admin-only — checks role before writing. + */ +export async function saveLicServerConfig( + base_url: string, + api_key: string +): Promise<{ success: boolean; error?: string }> { + const supabase = await createClient() + + // Auth + role check + const { data: { user } } = await supabase.auth.getUser() + if (!user) return { success: false, error: 'Nicht angemeldet' } + + const { data: userData } = await supabase + .from('users') + .select('role') + .eq('id', user.id) + .single() + + if (!userData || !['admin', 'superadmin'].includes(userData.role)) { + return { success: false, error: 'Keine Berechtigung' } + } + + const { error } = await supabase + .from('settings') + .upsert({ + id: 'licserver', + licserver_base_url: base_url.trim() || null, + licserver_api_key: api_key.trim() || null, + updated_at: new Date().toISOString(), + }) + + if (error) return { success: false, error: error.message } + return { success: true } +} + +/** + * Test the connection with current config. + * Calls GET /api-v1/partners?pageSize=1 and returns true if 200. + */ +export async function testLicServerConnection(): Promise<{ ok: boolean; message: string }> { + const cfg = await getLicServerConfig() + + if (!cfg.base_url || !cfg.api_key) { + return { ok: false, message: 'URL oder API-Key nicht konfiguriert' } + } + + try { + const res = await fetch(`${cfg.base_url}/api-v1/partners?pageSize=1`, { + headers: { 'X-Api-Key': cfg.api_key, Accept: 'application/json' }, + signal: AbortSignal.timeout(5_000), + }) + if (res.ok) return { ok: true, message: `Verbindung OK (HTTP ${res.status})` } + return { ok: false, message: `LicServer antwortete mit HTTP ${res.status}` } + } catch (err: any) { + return { ok: false, message: `Nicht erreichbar: ${err.message}` } + } +} diff --git a/shop/supabase/migrations/20260714000000_add_licserver_config_to_settings.sql b/shop/supabase/migrations/20260714000000_add_licserver_config_to_settings.sql new file mode 100644 index 0000000..6fcdad5 --- /dev/null +++ b/shop/supabase/migrations/20260714000000_add_licserver_config_to_settings.sql @@ -0,0 +1,11 @@ +-- Migration: Add LicServer configuration columns to settings table +ALTER TABLE public.settings + ADD COLUMN IF NOT EXISTS licserver_base_url TEXT, + ADD COLUMN IF NOT EXISTS licserver_api_key TEXT; + +-- Ensure the licserver config row exists (upsert with empty defaults) +INSERT INTO public.settings (id, licserver_base_url, licserver_api_key) +VALUES ('licserver', NULL, NULL) +ON CONFLICT (id) DO NOTHING; + +NOTIFY pgrst, 'reload schema';