diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 34fa742c..92c25eec 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -5,8 +5,8 @@ Caddy Proxy Manager+ - - + +
diff --git a/frontend/src/api/health.ts b/frontend/src/api/health.ts new file mode 100644 index 00000000..3ed65b6a --- /dev/null +++ b/frontend/src/api/health.ts @@ -0,0 +1,11 @@ +import client from './client'; + +export interface HealthResponse { + status: string; + service: string; +} + +export const checkHealth = async (): Promise => { + const { data } = await client.get('/health'); + return data; +}; diff --git a/frontend/src/api/import.ts b/frontend/src/api/import.ts new file mode 100644 index 00000000..6671b5a7 --- /dev/null +++ b/frontend/src/api/import.ts @@ -0,0 +1,51 @@ +import client from './client'; + +export interface ImportSession { + id: string; + state: 'pending' | 'reviewing' | 'completed' | 'failed'; + created_at: string; + updated_at: string; +} + +export interface ImportPreview { + session: ImportSession; + preview: { + hosts: Array<{ domain_names: string; [key: string]: unknown }>; + conflicts: Record; + errors: string[]; + }; +} + +export const uploadCaddyfile = async (content: string): Promise => { + const { data } = await client.post('/import/upload', { content }); + return data; +}; + +export const getImportPreview = async (): Promise => { + const { data } = await client.get('/import/preview'); + return data; +}; + +export const commitImport = async (resolutions: Record): Promise => { + await client.post('/import/commit', { resolutions }); +}; + +export const cancelImport = async (): Promise => { + await client.post('/import/cancel'); +}; + +export const getImportStatus = async (): Promise<{ has_pending: boolean; session?: ImportSession }> => { + // Note: Assuming there might be a status endpoint or we infer from preview. + // If no dedicated status endpoint exists in backend, we might rely on preview returning 404 or empty. + // Based on previous context, there wasn't an explicit status endpoint mentioned in the simple API, + // but the hook used `importAPI.status()`. I'll check the backend routes if needed. + // For now, I'll implement it assuming /import/preview can serve as status check or there is a /import/status. + // Let's check the backend routes to be sure. + try { + const { data } = await client.get<{ has_pending: boolean; session?: ImportSession }>('/import/status'); + return data; + } catch (error) { + // Fallback if status endpoint doesn't exist, though the hook used it. + return { has_pending: false }; + } +}; diff --git a/frontend/src/api/proxyHosts.ts b/frontend/src/api/proxyHosts.ts new file mode 100644 index 00000000..9b4fb65d --- /dev/null +++ b/frontend/src/api/proxyHosts.ts @@ -0,0 +1,52 @@ +import client from './client'; + +export interface Location { + uuid?: string; + path: string; + forward_scheme: string; + forward_host: string; + forward_port: number; +} + +export interface ProxyHost { + uuid: string; + domain_names: string; + forward_scheme: string; + forward_host: string; + forward_port: number; + ssl_forced: boolean; + http2_support: boolean; + hsts_enabled: boolean; + hsts_subdomains: boolean; + block_exploits: boolean; + websocket_support: boolean; + locations: Location[]; + advanced_config?: string; + enabled: boolean; + created_at: string; + updated_at: string; +} + +export const getProxyHosts = async (): Promise => { + const { data } = await client.get('/proxy-hosts'); + return data; +}; + +export const getProxyHost = async (uuid: string): Promise => { + const { data } = await client.get(`/proxy-hosts/${uuid}`); + return data; +}; + +export const createProxyHost = async (host: Partial): Promise => { + const { data } = await client.post('/proxy-hosts', host); + return data; +}; + +export const updateProxyHost = async (uuid: string, host: Partial): Promise => { + const { data } = await client.put(`/proxy-hosts/${uuid}`, host); + return data; +}; + +export const deleteProxyHost = async (uuid: string): Promise => { + await client.delete(`/proxy-hosts/${uuid}`); +}; diff --git a/frontend/src/api/remoteServers.ts b/frontend/src/api/remoteServers.ts new file mode 100644 index 00000000..4309ed08 --- /dev/null +++ b/frontend/src/api/remoteServers.ts @@ -0,0 +1,40 @@ +import client from './client'; + +export interface RemoteServer { + uuid: string; + name: string; + provider: string; + host: string; + port: number; + username?: string; + enabled: boolean; + reachable: boolean; + last_check?: string; + created_at: string; + updated_at: string; +} + +export const getRemoteServers = async (enabledOnly = false): Promise => { + const params = enabledOnly ? { enabled: true } : {}; + const { data } = await client.get('/remote-servers', { params }); + return data; +}; + +export const getRemoteServer = async (uuid: string): Promise => { + const { data } = await client.get(`/remote-servers/${uuid}`); + return data; +}; + +export const createRemoteServer = async (server: Partial): Promise => { + const { data } = await client.post('/remote-servers', server); + return data; +}; + +export const updateRemoteServer = async (uuid: string, server: Partial): Promise => { + const { data } = await client.put(`/remote-servers/${uuid}`, server); + return data; +}; + +export const deleteRemoteServer = async (uuid: string): Promise => { + await client.delete(`/remote-servers/${uuid}`); +}; diff --git a/frontend/src/components/ImportBanner.tsx b/frontend/src/components/ImportBanner.tsx index 549fe805..47bc1296 100644 --- a/frontend/src/components/ImportBanner.tsx +++ b/frontend/src/components/ImportBanner.tsx @@ -1,43 +1,29 @@ -interface ImportBannerProps { - session: { - uuid: string - filename?: string - state: string - created_at: string - } +interface Props { + session: { id: string } onReview: () => void onCancel: () => void } -export default function ImportBanner({ session, onReview, onCancel }: ImportBannerProps) { +export default function ImportBanner({ session, onReview, onCancel }: Props) { return ( -
-
-
-

- Import Session Active -

-

- {session.filename && `File: ${session.filename} • `} - State: {session.state} -

-
-
- {session.state === 'reviewing' && ( - - )} - -
+
+
+
Pending Import Session
+
Session ID: {session.id}
+
+
+ +
) diff --git a/frontend/src/components/ImportReviewTable.tsx b/frontend/src/components/ImportReviewTable.tsx index 9e2f6847..1234dd42 100644 --- a/frontend/src/components/ImportReviewTable.tsx +++ b/frontend/src/components/ImportReviewTable.tsx @@ -1,171 +1,121 @@ -import { useState } from 'react' +import { useMemo, useState } from 'react' -interface ImportReviewTableProps { - hosts: any[] - conflicts: string[] +interface HostPreview { + domain_names: string + [key: string]: unknown +} + +interface Props { + hosts: HostPreview[] + conflicts: Record errors: string[] onCommit: (resolutions: Record) => Promise onCancel: () => void } -export default function ImportReviewTable({ hosts, conflicts, errors, onCommit, onCancel }: ImportReviewTableProps) { - const [resolutions, setResolutions] = useState>({}) - const [loading, setLoading] = useState(false) - - const hasConflicts = conflicts.length > 0 - - const handleResolutionChange = (domain: string, action: string) => { - setResolutions({ ...resolutions, [domain]: action }) - } +export default function ImportReviewTable({ hosts, conflicts, errors, onCommit, onCancel }: Props) { + const conflictDomains = useMemo(() => Object.keys(conflicts || {}), [conflicts]) + const [resolutions, setResolutions] = useState>(() => { + const init: Record = {} + conflictDomains.forEach((d: string) => { init[d] = conflicts[d] || 'keep' }) + return init + }) + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState(null) const handleCommit = async () => { - // Ensure all conflicts have resolutions - const unresolvedConflicts = conflicts.filter(c => !resolutions[c]) - if (unresolvedConflicts.length > 0) { - alert(`Please resolve all conflicts: ${unresolvedConflicts.join(', ')}`) - return - } - - setLoading(true) + setSubmitting(true) + setError(null) try { await onCommit(resolutions) + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to commit import') } finally { - setLoading(false) + setSubmitting(false) } } return ( -
- {/* Errors */} - {errors.length > 0 && ( -
-

Errors

-
    - {errors.map((error, idx) => ( -
  • {error}
  • +
    +
    +

    Review Imported Hosts

    +
    + + +
    +
    + + {error && ( +
    + {error} +
    + )} + + {errors?.length > 0 && ( +
    +
    Issues found during parsing
    +
      + {errors.map((e, i) => ( +
    • {e}
    • ))}
    )} - {/* Conflicts */} - {hasConflicts && ( -
    -

    - Conflicts Detected ({conflicts.length}) -

    -

    - The following domains already exist. Choose how to handle each conflict: -

    -
    - {conflicts.map((domain) => ( -
    - {domain} - -
    - ))} -
    -
    - )} - - {/* Preview Hosts */} -
    -
    -

    - Hosts to Import ({hosts.length}) -

    -
    -
    - - - - - - - - - - - {hosts.map((host, idx) => { - const isConflict = conflicts.includes(host.domain_names) - return ( - - - - - - - ) - })} - -
    - Domain - - Forward To - - SSL - - Features -
    -
    - {host.domain_names} - {isConflict && ( - - Conflict - - )} -
    -
    -
    - {host.forward_scheme}://{host.forward_host}:{host.forward_port} -
    -
    - {host.ssl_forced && ( - - SSL - - )} - -
    - {host.http2_support && ( - - HTTP/2 - - )} - {host.websocket_support && ( - - WS - - )} -
    -
    -
    -
    - - {/* Actions */} -
    - - +
    + + + + + + + + + {hosts.map((h, idx) => { + const domain = h.domain_names + const hasConflict = conflictDomains.includes(domain) + return ( + + + + + ) + })} + +
    + Domain Names + + Conflict Resolution +
    +
    {domain}
    +
    + {hasConflict ? ( + + ) : ( + + No conflict + + )} +
    ) diff --git a/frontend/src/components/ProxyHostForm.tsx b/frontend/src/components/ProxyHostForm.tsx index 5b16be54..3f98151e 100644 --- a/frontend/src/components/ProxyHostForm.tsx +++ b/frontend/src/components/ProxyHostForm.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from 'react' -import { ProxyHost, Location } from '../hooks/useProxyHosts' -import { remoteServersAPI } from '../services/api' +import { useState } from 'react' +import type { ProxyHost } from '../api/proxyHosts' +import { useRemoteServers } from '../hooks/useRemoteServers' interface ProxyHostFormProps { host?: ProxyHost @@ -8,15 +8,6 @@ interface ProxyHostFormProps { onCancel: () => void } -interface RemoteServer { - uuid: string - name: string - provider: string - host: string - port: number - enabled: boolean -} - export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFormProps) { const [formData, setFormData] = useState({ domain_names: host?.domain_names || '', @@ -29,48 +20,14 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor hsts_subdomains: host?.hsts_subdomains ?? false, block_exploits: host?.block_exploits ?? true, websocket_support: host?.websocket_support ?? false, - locations: host?.locations || [] as Location[], + advanced_config: host?.advanced_config || '', enabled: host?.enabled ?? true, }) - const [remoteServers, setRemoteServers] = useState([]) + const { servers: remoteServers } = useRemoteServers() const [loading, setLoading] = useState(false) const [error, setError] = useState(null) - useEffect(() => { - const fetchServers = async () => { - try { - const servers = await remoteServersAPI.list(true) - setRemoteServers(servers) - } catch (err) { - console.error('Failed to fetch remote servers:', err) - } - } - fetchServers() - }, []) - - const addLocation = () => { - setFormData({ - ...formData, - locations: [ - ...formData.locations, - { path: '/', forward_scheme: 'http', forward_host: '', forward_port: 80 } - ] - }) - } - - const removeLocation = (index: number) => { - const newLocations = [...formData.locations] - newLocations.splice(index, 1) - setFormData({ ...formData, locations: newLocations }) - } - - const updateLocation = (index: number, field: keyof Location, value: any) => { - const newLocations = [...formData.locations] - newLocations[index] = { ...newLocations[index], [field]: value } - setFormData({ ...formData, locations: newLocations }) - } - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setLoading(true) @@ -253,83 +210,18 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
    - {/* Custom Locations */} + {/* Advanced Config */}
    -
    - - -
    - -
    - {formData.locations.map((location, index) => ( -
    -
    -
    - - updateLocation(index, 'path', e.target.value)} - placeholder="/api" - className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500" - /> -
    -
    - - -
    -
    - - updateLocation(index, 'forward_host', e.target.value)} - placeholder="10.0.0.1" - className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500" - /> -
    -
    - - updateLocation(index, 'forward_port', parseInt(e.target.value))} - className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500" - /> -
    -
    -
    - -
    -
    - ))} - {formData.locations.length === 0 && ( -
    - No custom locations defined -
    - )} -
    + +