feat(licserver): add partner customers proxy API
This commit is contained in:
@@ -21,14 +21,14 @@ export default function AdminSettings() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
// LicServer Config State
|
// LicServer Config State
|
||||||
const [licUrl, setLicUrl] = useState('');
|
const [licUrl, setLicUrl] = useState('');
|
||||||
const [licKey, setLicKey] = useState('');
|
const [licKey, setLicKey] = useState('');
|
||||||
const [showKey, setShowKey] = useState(false);
|
const [showKey, setShowKey] = useState(false);
|
||||||
const [licSaving, setLicSaving] = useState(false);
|
const [licSaving, setLicSaving] = useState(false);
|
||||||
const [licTesting, setLicTesting] = useState(false);
|
const [licTesting, setLicTesting] = useState(false);
|
||||||
const [licStatus, setLicStatus] = useState<{ ok: boolean; message: string } | null>(null);
|
const [licStatus, setLicStatus] = useState<{ ok: boolean; message: string } | null>(null);
|
||||||
const [licMsg, setLicMsg] = useState('');
|
const [licMsg, setLicMsg] = useState('');
|
||||||
const [licMsgType, setLicMsgType] = useState<'success' | 'error' | ''>('');
|
const [licMsgType, setLicMsgType] = useState<'success' | 'error' | ''>('');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -63,7 +63,7 @@ export default function AdminSettings() {
|
|||||||
.single();
|
.single();
|
||||||
if (licRow) {
|
if (licRow) {
|
||||||
setLicUrl(licRow.licserver_base_url || '');
|
setLicUrl(licRow.licserver_base_url || '');
|
||||||
setLicKey(licRow.licserver_api_key || '');
|
setLicKey(licRow.licserver_api_key || '');
|
||||||
}
|
}
|
||||||
} catch (dbErr) {
|
} catch (dbErr) {
|
||||||
console.error("Fehler beim Laden der LicServer-Einstellungen:", dbErr);
|
console.error("Fehler beim Laden der LicServer-Einstellungen:", dbErr);
|
||||||
@@ -90,7 +90,7 @@ export default function AdminSettings() {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch('/api/admin/db-backup');
|
const res = await fetch('/api/admin/db-backup');
|
||||||
if (!res.ok) throw new Error('Export fehlgeschlagen');
|
if (!res.ok) throw new Error('Export fehlgeschlagen');
|
||||||
|
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
@@ -184,11 +184,10 @@ export default function AdminSettings() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{statusMsg && (
|
{statusMsg && (
|
||||||
<div className={`p-4 rounded-lg text-sm border ${
|
<div className={`p-4 rounded-lg text-sm border ${statusType === 'success' ? 'bg-green-500/10 border-green-500/20 text-green-600 dark:text-green-400' :
|
||||||
statusType === 'success' ? 'bg-green-500/10 border-green-500/20 text-green-600 dark:text-green-400' :
|
|
||||||
statusType === 'error' ? 'bg-destructive/10 border-destructive/20 text-destructive' :
|
statusType === 'error' ? 'bg-destructive/10 border-destructive/20 text-destructive' :
|
||||||
'bg-blue-500/10 border-blue-500/20 text-blue-600 dark:text-blue-400'
|
'bg-blue-500/10 border-blue-500/20 text-blue-600 dark:text-blue-400'
|
||||||
}`}>
|
}`}>
|
||||||
{statusMsg}
|
{statusMsg}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -205,8 +204,8 @@ export default function AdminSettings() {
|
|||||||
Lädt alle Kategorien, Produkte, Module, Firmen, Endkunden, Bestellungen und SMTP-Einstellungen als ZIP-Datei herunter.
|
Lädt alle Kategorien, Produkte, Module, Firmen, Endkunden, Bestellungen und SMTP-Einstellungen als ZIP-Datei herunter.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
disabled={exporting || importing}
|
disabled={exporting || importing}
|
||||||
className="w-full bg-primary hover:bg-primary/90 text-white"
|
className="w-full bg-primary hover:bg-primary/90 text-white"
|
||||||
>
|
>
|
||||||
@@ -235,9 +234,9 @@ export default function AdminSettings() {
|
|||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="relative border border-dashed border-slate-200 dark:border-white/10 rounded-lg p-3 hover:bg-slate-100/50 dark:hover:bg-white/5 transition">
|
<div className="relative border border-dashed border-slate-200 dark:border-white/10 rounded-lg p-3 hover:bg-slate-100/50 dark:hover:bg-white/5 transition">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".zip"
|
accept=".zip"
|
||||||
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
|
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
|
||||||
disabled={exporting || importing}
|
disabled={exporting || importing}
|
||||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
|
||||||
@@ -251,8 +250,8 @@ export default function AdminSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleImport}
|
onClick={handleImport}
|
||||||
disabled={!selectedFile || exporting || importing}
|
disabled={!selectedFile || exporting || importing}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="w-full border border-slate-200 dark:border-white/10"
|
className="w-full border border-slate-200 dark:border-white/10"
|
||||||
@@ -275,29 +274,26 @@ export default function AdminSettings() {
|
|||||||
LicServer Konfiguration
|
LicServer Konfiguration
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-slate-500 dark:text-slate-400 text-xs mt-1">
|
<p className="text-slate-500 dark:text-slate-400 text-xs mt-1">
|
||||||
Verbindungseinstellungen zum CASPOS Lizenzserver (internes Container-Netz).
|
Verbindungseinstellungen zum CASPOS Lizenzserver.
|
||||||
Der API-Key wird verschlüsselt in der Datenbank gespeichert und nie an den Browser übertragen.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Message */}
|
{/* Status Message */}
|
||||||
{licMsg && (
|
{licMsg && (
|
||||||
<div className={`p-3 rounded-lg text-sm border ${
|
<div className={`p-3 rounded-lg text-sm border ${licMsgType === 'success'
|
||||||
licMsgType === 'success'
|
? 'bg-green-500/10 border-green-500/20 text-green-600 dark:text-green-400'
|
||||||
? 'bg-green-500/10 border-green-500/20 text-green-600 dark:text-green-400'
|
: 'bg-destructive/10 border-destructive/20 text-destructive'
|
||||||
: 'bg-destructive/10 border-destructive/20 text-destructive'
|
}`}>
|
||||||
}`}>
|
|
||||||
{licMsg}
|
{licMsg}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Connection Test Result */}
|
{/* Connection Test Result */}
|
||||||
{licStatus && (
|
{licStatus && (
|
||||||
<div className={`flex items-center gap-2 p-3 rounded-lg text-sm border ${
|
<div className={`flex items-center gap-2 p-3 rounded-lg text-sm border ${licStatus.ok
|
||||||
licStatus.ok
|
? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-600 dark:text-emerald-400'
|
||||||
? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-600 dark:text-emerald-400'
|
: 'bg-amber-500/10 border-amber-500/20 text-amber-600 dark:text-amber-400'
|
||||||
: 'bg-amber-500/10 border-amber-500/20 text-amber-600 dark:text-amber-400'
|
}`}>
|
||||||
}`}>
|
|
||||||
{licStatus.ok
|
{licStatus.ok
|
||||||
? <CheckCircle2 className="w-4 h-4 shrink-0" />
|
? <CheckCircle2 className="w-4 h-4 shrink-0" />
|
||||||
: <XCircle className="w-4 h-4 shrink-0" />}
|
: <XCircle className="w-4 h-4 shrink-0" />}
|
||||||
@@ -318,7 +314,7 @@ export default function AdminSettings() {
|
|||||||
placeholder="http://192.168.178.174:9980"
|
placeholder="http://192.168.178.174:9980"
|
||||||
className="text-sm font-mono"
|
className="text-sm font-mono"
|
||||||
/>
|
/>
|
||||||
<p className="text-[10px] text-slate-400">Interne Container-URL ohne trailing slash</p>
|
<p className="text-[10px] text-slate-400">URL des CASPOS Lizenzservers</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* API Key */}
|
{/* API Key */}
|
||||||
@@ -344,51 +340,51 @@ export default function AdminSettings() {
|
|||||||
{showKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
{showKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-slate-400">Wird als X-Api-Key Header an den LicServer gesendet</p>
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-3 pt-1">
|
<div className="flex gap-3 pt-1">
|
||||||
<Button
|
<Button
|
||||||
id="lic-save-btn"
|
id="lic-save-btn"
|
||||||
disabled={licSaving || licTesting}
|
disabled={licSaving || licTesting}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLicSaving(true);
|
setLicSaving(true);
|
||||||
setLicMsg('');
|
setLicMsg('');
|
||||||
setLicStatus(null);
|
setLicStatus(null);
|
||||||
const res = await saveLicServerConfig(licUrl, licKey);
|
const res = await saveLicServerConfig(licUrl, licKey);
|
||||||
setLicMsgType(res.success ? 'success' : 'error');
|
setLicMsgType(res.success ? 'success' : 'error');
|
||||||
setLicMsg(res.success ? 'Konfiguration gespeichert.' : (res.error || 'Fehler beim Speichern'));
|
setLicMsg(res.success ? 'Konfiguration gespeichert.' : (res.error || 'Fehler beim Speichern'));
|
||||||
setLicSaving(false);
|
setLicSaving(false);
|
||||||
}}
|
}}
|
||||||
className="bg-violet-600 hover:bg-violet-500 text-white"
|
className="bg-violet-600 hover:bg-violet-500 text-white"
|
||||||
>
|
>
|
||||||
{licSaving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <KeyRound className="w-4 h-4 mr-2" />}
|
{licSaving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <KeyRound className="w-4 h-4 mr-2" />}
|
||||||
Speichern
|
Speichern
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
id="lic-test-btn"
|
id="lic-test-btn"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={licSaving || licTesting}
|
disabled={licSaving || licTesting}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLicTesting(true);
|
setLicTesting(true);
|
||||||
setLicStatus(null);
|
setLicStatus(null);
|
||||||
// Save first, then test
|
// Save first, then test
|
||||||
await saveLicServerConfig(licUrl, licKey);
|
await saveLicServerConfig(licUrl, licKey);
|
||||||
const result = await testLicServerConnection();
|
const result = await testLicServerConnection();
|
||||||
setLicStatus(result);
|
setLicStatus(result);
|
||||||
setLicTesting(false);
|
setLicTesting(false);
|
||||||
}}
|
}}
|
||||||
className="border-white/10"
|
className="border-white/10"
|
||||||
>
|
>
|
||||||
{licTesting ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Wifi className="w-4 h-4 mr-2" />}
|
{licTesting ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Wifi className="w-4 h-4 mr-2" />}
|
||||||
Verbindung testen
|
Verbindung testen
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
85
shop/app/api/licserver/partners/[id]/customers/route.ts
Normal file
85
shop/app/api/licserver/partners/[id]/customers/route.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { createClient } from '@/lib/supabase/server'
|
||||||
|
import { getLicServerConfig } from '@/lib/actions/licserver-config'
|
||||||
|
import type { CustomerDtoPagedResult, LicServerProblemDetails } from '@/lib/types/licserver'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/licserver/partners/[id]/customers
|
||||||
|
*
|
||||||
|
* Proxy to LicServer GET /api-v1/partners/{id}/customers
|
||||||
|
* [id] must be a valid UUID.
|
||||||
|
* Query params forwarded: search, page, pageSize
|
||||||
|
*
|
||||||
|
* 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,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
// ── Auth guard ─────────────────────────────────────────────────────────
|
||||||
|
const supabase = await createClient()
|
||||||
|
const { data: { user } } = await supabase.auth.getUser()
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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 nicht konfiguriert. Bitte in den Admin-Einstellungen hinterlegen.' },
|
||||||
|
{ status: 503 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params
|
||||||
|
|
||||||
|
// Basic UUID format guard
|
||||||
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||||
|
if (!UUID_RE.test(id)) {
|
||||||
|
return NextResponse.json({ error: 'Invalid partner ID' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Forward query params ───────────────────────────────────────────────
|
||||||
|
const { searchParams } = req.nextUrl
|
||||||
|
const upstream = new URL(`${cfg.base_url}/api-v1/partners/${id}/customers`)
|
||||||
|
const search = searchParams.get('search')
|
||||||
|
const page = searchParams.get('page')
|
||||||
|
const pageSize = searchParams.get('pageSize')
|
||||||
|
if (search) upstream.searchParams.set('search', search)
|
||||||
|
if (page) upstream.searchParams.set('page', page)
|
||||||
|
if (pageSize) upstream.searchParams.set('pageSize', pageSize)
|
||||||
|
|
||||||
|
// ── Proxy request ──────────────────────────────────────────────────────
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
upstream.toString(),
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-Api-Key': cfg.api_key,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(5_000),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const problem: LicServerProblemDetails = await res.json().catch(() => ({}))
|
||||||
|
return NextResponse.json(problem, { status: res.status })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: CustomerDtoPagedResult = await res.json()
|
||||||
|
return NextResponse.json(data)
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[licserver] partner/:id/customers fetch error:', err)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'LicServer nicht erreichbar' },
|
||||||
|
{ status: 502 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,3 +29,60 @@ export interface LicServerProblemDetails {
|
|||||||
instance?: string | null
|
instance?: string | null
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CustomerDto {
|
||||||
|
id: string // UUID
|
||||||
|
name: string | null
|
||||||
|
street: string | null
|
||||||
|
houseNumber: string | null
|
||||||
|
houseNumberAddon: string | null
|
||||||
|
zipCode: string | null
|
||||||
|
city: string | null
|
||||||
|
countryCode: string | null
|
||||||
|
email: string | null
|
||||||
|
partnerId: string | null
|
||||||
|
erpId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerDtoPagedResult {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
totalCount: number
|
||||||
|
totalPages: number
|
||||||
|
hasNextPage: boolean
|
||||||
|
hasPreviousPage: boolean
|
||||||
|
items: CustomerDto[] | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationDto {
|
||||||
|
id: string // UUID
|
||||||
|
name: string | null
|
||||||
|
street: string | null
|
||||||
|
houseNumber: string | null
|
||||||
|
houseNumberAddon: string | null
|
||||||
|
countryCode: string | null
|
||||||
|
postalCode: string | null
|
||||||
|
city: string | null
|
||||||
|
taxNumber: string | null
|
||||||
|
vatIdNumber: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerDetailsDto {
|
||||||
|
id: string // UUID
|
||||||
|
name: string | null
|
||||||
|
street: string | null
|
||||||
|
houseNumber: string | null
|
||||||
|
houseNumberAddon: string | null
|
||||||
|
zipCode: string | null
|
||||||
|
city: string | null
|
||||||
|
countryCode: string | null
|
||||||
|
email: string | null
|
||||||
|
partnerId: string | null
|
||||||
|
erpId: string | null
|
||||||
|
partnerName: string | null
|
||||||
|
taxNumber: string | null
|
||||||
|
vatIdNumber: string | null
|
||||||
|
notes: string | null
|
||||||
|
locations: LocationDto[] | null
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user