1+ import { NextRequest , NextResponse } from 'next/server' ;
2+ import { getServiceUrls } from '../../lib/k8sClient' ;
3+
4+ const ALLOWED_SERVICES = [ 'grafana' , 'kiali' , 'jaeger' , 'argocd' , 'prometheus' , 'alertmanager' ] ;
5+ const REQUEST_TIMEOUT = 10000 ; // 10 seconds
6+
7+ export async function GET ( request : NextRequest , context : { params : Promise < { service : string [ ] } > } ) {
8+ const params = await context . params ;
9+ return handleServiceRequest ( request , params . service , 'GET' ) ;
10+ }
11+
12+ export async function POST ( request : NextRequest , context : { params : Promise < { service : string [ ] } > } ) {
13+ const params = await context . params ;
14+ return handleServiceRequest ( request , params . service , 'POST' ) ;
15+ }
16+
17+ export async function PUT ( request : NextRequest , context : { params : Promise < { service : string [ ] } > } ) {
18+ const params = await context . params ;
19+ return handleServiceRequest ( request , params . service , 'PUT' ) ;
20+ }
21+
22+ export async function DELETE ( request : NextRequest , context : { params : Promise < { service : string [ ] } > } ) {
23+ const params = await context . params ;
24+ return handleServiceRequest ( request , params . service , 'DELETE' ) ;
25+ }
26+
27+ async function handleServiceRequest (
28+ request : NextRequest ,
29+ serviceSegments : string [ ] ,
30+ method : string
31+ ) {
32+ try {
33+ const [ service , ...pathSegments ] = serviceSegments ;
34+
35+ // Validate service
36+ if ( ! ALLOWED_SERVICES . includes ( service ) ) {
37+ return NextResponse . json (
38+ { error : 'Service not allowed' } ,
39+ { status : 403 }
40+ ) ;
41+ }
42+
43+ // Get service URLs
44+ const serviceUrls = getServiceUrls ( ) ;
45+ const baseUrl = serviceUrls [ service as keyof typeof serviceUrls ] ;
46+
47+ if ( ! baseUrl ) {
48+ return NextResponse . json (
49+ { error : 'Service URL not found' } ,
50+ { status : 404 }
51+ ) ;
52+ }
53+
54+ // Build target URL
55+ const targetPath = pathSegments . length > 0 ? pathSegments . join ( '/' ) : '' ;
56+ const url = new URL ( request . url ) ;
57+ const queryString = url . searchParams . toString ( ) ;
58+ const targetUrl = `${ baseUrl } /${ targetPath } ${ queryString ? `?${ queryString } ` : '' } ` ;
59+
60+ // Prepare headers and pass through authentication
61+ const headers = new Headers ( ) ;
62+ headers . set ( 'User-Agent' , 'WindeathAdmin/1.0' ) ;
63+ headers . set ( 'Accept' , request . headers . get ( 'accept' ) || 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' ) ;
64+
65+ if ( request . headers . get ( 'content-type' ) ) {
66+ headers . set ( 'Content-Type' , request . headers . get ( 'content-type' ) ! ) ;
67+ }
68+
69+ // Pass through cookies for authentication
70+ if ( request . headers . get ( 'cookie' ) ) {
71+ headers . set ( 'Cookie' , request . headers . get ( 'cookie' ) ! ) ;
72+ }
73+
74+ // Pass through authorization header if present
75+ if ( request . headers . get ( 'authorization' ) ) {
76+ headers . set ( 'Authorization' , request . headers . get ( 'authorization' ) ! ) ;
77+ }
78+
79+ // Prepare request body for POST/PUT
80+ let body : string | undefined ;
81+ if ( method === 'POST' || method === 'PUT' ) {
82+ body = await request . text ( ) ;
83+ }
84+
85+ // Create AbortController for timeout
86+ const controller = new AbortController ( ) ;
87+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , REQUEST_TIMEOUT ) ;
88+
89+ try {
90+ // Make the request
91+ const response = await fetch ( targetUrl , {
92+ method,
93+ headers,
94+ body,
95+ signal : controller . signal ,
96+ } ) ;
97+
98+ clearTimeout ( timeoutId ) ;
99+
100+ // Handle different content types
101+ const contentType = response . headers . get ( 'content-type' ) ;
102+
103+ if ( contentType ?. includes ( 'text/html' ) ) {
104+ // For HTML responses (like dashboard pages), return as-is but modify any relative URLs
105+ let html = await response . text ( ) ;
106+
107+ // Replace relative URLs with admin-prefixed URLs for proper routing
108+ html = html . replace ( / h r e f = " \/ (? ! a d m i n ) / g, `href="/admin/${ service } /` ) ;
109+ html = html . replace ( / s r c = " \/ (? ! a d m i n ) / g, `src="/admin/${ service } /` ) ;
110+ html = html . replace ( / a c t i o n = " \/ (? ! a d m i n ) / g, `action="/admin/${ service } /` ) ;
111+
112+ return new NextResponse ( html , {
113+ status : response . status ,
114+ headers : {
115+ 'Content-Type' : contentType ,
116+ 'Cache-Control' : 'no-cache, no-store, must-revalidate' ,
117+ } ,
118+ } ) ;
119+ } else if ( contentType ?. includes ( 'application/json' ) ) {
120+ const data = await response . json ( ) ;
121+ return NextResponse . json ( data , {
122+ status : response . status ,
123+ headers : {
124+ 'Content-Type' : 'application/json' ,
125+ 'Cache-Control' : 'no-cache, no-store, must-revalidate' ,
126+ } ,
127+ } ) ;
128+ } else if ( contentType ?. includes ( 'text/' ) ) {
129+ const text = await response . text ( ) ;
130+ return new NextResponse ( text , {
131+ status : response . status ,
132+ headers : {
133+ 'Content-Type' : contentType ,
134+ 'Cache-Control' : 'no-cache, no-store, must-revalidate' ,
135+ } ,
136+ } ) ;
137+ } else {
138+ // Handle binary data (CSS, JS, images, etc.)
139+ const buffer = await response . arrayBuffer ( ) ;
140+ return new NextResponse ( buffer , {
141+ status : response . status ,
142+ headers : {
143+ 'Content-Type' : contentType || 'application/octet-stream' ,
144+ 'Cache-Control' : 'no-cache, no-store, must-revalidate' ,
145+ } ,
146+ } ) ;
147+ }
148+ } catch ( fetchError ) {
149+ clearTimeout ( timeoutId ) ;
150+
151+ if ( fetchError instanceof Error && fetchError . name === 'AbortError' ) {
152+ return NextResponse . json (
153+ { error : 'Request timeout' } ,
154+ { status : 504 }
155+ ) ;
156+ }
157+ throw fetchError ;
158+ }
159+ } catch ( error ) {
160+ console . error ( 'Service proxy request failed:' , error ) ;
161+
162+ return NextResponse . json (
163+ {
164+ error : 'Service proxy request failed' ,
165+ details : error instanceof Error ? error . message : 'Unknown error' ,
166+ service : serviceSegments [ 0 ] ,
167+ } ,
168+ { status : 500 }
169+ ) ;
170+ }
171+ }
172+
173+ // Health check endpoint
174+ export async function HEAD ( request : NextRequest , context : { params : Promise < { service : string [ ] } > } ) {
175+ const params = await context . params ;
176+ try {
177+ const [ service ] = params . service ;
178+
179+ if ( ! ALLOWED_SERVICES . includes ( service ) ) {
180+ return new NextResponse ( null , { status : 403 } ) ;
181+ }
182+
183+ const serviceUrls = getServiceUrls ( ) ;
184+ const baseUrl = serviceUrls [ service as keyof typeof serviceUrls ] ;
185+
186+ if ( ! baseUrl ) {
187+ return new NextResponse ( null , { status : 404 } ) ;
188+ }
189+
190+ // Simple connectivity check
191+ const controller = new AbortController ( ) ;
192+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , 5000 ) ;
193+
194+ try {
195+ const response = await fetch ( baseUrl , {
196+ method : 'HEAD' ,
197+ signal : controller . signal ,
198+ } ) ;
199+
200+ clearTimeout ( timeoutId ) ;
201+
202+ return new NextResponse ( null , {
203+ status : response . ok ? 200 : response . status ,
204+ } ) ;
205+ } catch ( fetchError ) {
206+ clearTimeout ( timeoutId ) ;
207+ return new NextResponse ( null , { status : 503 } ) ;
208+ }
209+ } catch ( error ) {
210+ console . error ( 'Health check failed:' , error ) ;
211+ return new NextResponse ( null , { status : 500 } ) ;
212+ }
213+ }
0 commit comments