@platformatic/gateway 3.0.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +13 -0
- package/config.d.ts +1000 -0
- package/eslint.config.js +12 -0
- package/index.d.ts +58 -0
- package/index.js +34 -0
- package/lib/application.js +186 -0
- package/lib/capability.js +89 -0
- package/lib/commands/index.js +15 -0
- package/lib/commands/openapi-fetch-schemas.js +48 -0
- package/lib/errors.js +18 -0
- package/lib/gateway-hook.js +66 -0
- package/lib/generator.js +127 -0
- package/lib/graphql-fetch.js +83 -0
- package/lib/graphql-generator.js +33 -0
- package/lib/graphql.js +29 -0
- package/lib/metrics.js +12 -0
- package/lib/not-host-constraints.js +31 -0
- package/lib/openapi-composer.js +101 -0
- package/lib/openapi-config-schema.js +89 -0
- package/lib/openapi-generator.js +215 -0
- package/lib/openapi-load-config.js +31 -0
- package/lib/openapi-modifier.js +131 -0
- package/lib/openapi-scalar.js +22 -0
- package/lib/proxy.js +266 -0
- package/lib/root.js +75 -0
- package/lib/schema.js +258 -0
- package/lib/upgrade.js +20 -0
- package/lib/utils.js +16 -0
- package/lib/versions/2.0.0.js +9 -0
- package/lib/versions/3.0.0.js +24 -0
- package/package.json +83 -0
- package/public/images/dark_mode.svg +3 -0
- package/public/images/ellipse.svg +21 -0
- package/public/images/external-link.svg +5 -0
- package/public/images/favicon.ico +0 -0
- package/public/images/graphiql.svg +10 -0
- package/public/images/graphql.svg +10 -0
- package/public/images/light_mode.svg +11 -0
- package/public/images/openapi.svg +13 -0
- package/public/images/platformatic-logo-dark.svg +30 -0
- package/public/images/platformatic-logo-light.svg +30 -0
- package/public/images/reverse-proxy.svg +8 -0
- package/public/images/triangle_dark.svg +3 -0
- package/public/images/triangle_light.svg +3 -0
- package/public/index.html +253 -0
- package/public/index.njk +101 -0
- package/public/main.css +244 -0
- package/schema.json +3779 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import traverse from 'json-schema-traverse'
|
|
2
|
+
import rfdc from 'rfdc'
|
|
3
|
+
|
|
4
|
+
const clone = rfdc()
|
|
5
|
+
|
|
6
|
+
const MODIFICATION_KEYWORDS = ['rename']
|
|
7
|
+
|
|
8
|
+
export const originPathSymbol = Symbol('originPath')
|
|
9
|
+
|
|
10
|
+
function findDataBySchemaPointer (schemaPointer, schema, data, parentData, callback) {
|
|
11
|
+
const schemaPointerParts = schemaPointer.split('/').slice(1)
|
|
12
|
+
|
|
13
|
+
for (const schemaPointerPart of schemaPointerParts) {
|
|
14
|
+
parentData = data
|
|
15
|
+
schema = schema[schemaPointerPart]
|
|
16
|
+
|
|
17
|
+
if (schemaPointerPart === 'properties') continue
|
|
18
|
+
|
|
19
|
+
if (schemaPointerPart === 'items') {
|
|
20
|
+
for (const item of data) {
|
|
21
|
+
const newSchemaPointer = '/' + schemaPointerParts.slice(1).join('/')
|
|
22
|
+
findDataBySchemaPointer(newSchemaPointer, schema, item, parentData, callback)
|
|
23
|
+
}
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
data = data[schemaPointerPart]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
callback(data, parentData)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getModificationRules (modificationSchema) {
|
|
34
|
+
const modificationRules = {}
|
|
35
|
+
|
|
36
|
+
function getModificationRules (schema, jsonPointer) {
|
|
37
|
+
const schemaKeys = Object.keys(schema)
|
|
38
|
+
const modificationKeys = schemaKeys.filter(key => MODIFICATION_KEYWORDS.includes(key))
|
|
39
|
+
|
|
40
|
+
if (modificationKeys.length === 0) return
|
|
41
|
+
modificationRules[jsonPointer] = schema
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
traverse(modificationSchema, { cb: getModificationRules })
|
|
45
|
+
return modificationRules
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function modifySchema (originSchema, modificationRules) {
|
|
49
|
+
function modifyOriginSchema (schema, jsonPointer, rs, psp, pk, parentSchema, keyIndex) {
|
|
50
|
+
const modificationRule = modificationRules[jsonPointer]
|
|
51
|
+
if (!modificationRule) return
|
|
52
|
+
|
|
53
|
+
if (modificationRule.rename) {
|
|
54
|
+
parentSchema.properties[modificationRule.rename] = schema
|
|
55
|
+
delete parentSchema.properties[keyIndex]
|
|
56
|
+
|
|
57
|
+
if (parentSchema.required) {
|
|
58
|
+
const index = parentSchema.required.indexOf(keyIndex)
|
|
59
|
+
if (index !== -1) {
|
|
60
|
+
parentSchema.required[index] = modificationRule.rename
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
traverse(originSchema, { cb: modifyOriginSchema })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function modifyPayload (payload, originSchema, modificationRules) {
|
|
69
|
+
for (const schemaJsonPointer in modificationRules) {
|
|
70
|
+
const rule = modificationRules[schemaJsonPointer]
|
|
71
|
+
|
|
72
|
+
findDataBySchemaPointer(schemaJsonPointer, originSchema, payload, null, (data, parentData) => {
|
|
73
|
+
if (rule.rename) {
|
|
74
|
+
parentData[rule.rename] = data
|
|
75
|
+
delete parentData[schemaJsonPointer.split('/').pop()]
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function modifyOpenApiSchema (app, schema, config) {
|
|
82
|
+
const newSchemaPaths = {}
|
|
83
|
+
const { paths } = clone(schema)
|
|
84
|
+
|
|
85
|
+
for (let path in paths) {
|
|
86
|
+
const pathConfig = config?.paths?.[path]
|
|
87
|
+
const pathSchema = paths[path]
|
|
88
|
+
|
|
89
|
+
if (pathConfig?.ignore) continue
|
|
90
|
+
|
|
91
|
+
pathSchema[originPathSymbol] = path
|
|
92
|
+
|
|
93
|
+
if (pathConfig?.alias) {
|
|
94
|
+
path = pathConfig.alias
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const method in pathSchema) {
|
|
98
|
+
const routeConfig = pathConfig?.[method.toLowerCase()]
|
|
99
|
+
|
|
100
|
+
if (routeConfig?.ignore) {
|
|
101
|
+
delete pathSchema[method]
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const modificationResponseSchema = routeConfig?.responses?.['200']
|
|
106
|
+
if (!modificationResponseSchema) continue
|
|
107
|
+
|
|
108
|
+
const modificationRules = getModificationRules(modificationResponseSchema)
|
|
109
|
+
|
|
110
|
+
function onRouteHook (routeOptions) {
|
|
111
|
+
const routeSchema = routeOptions.schema
|
|
112
|
+
const responseSchema = routeSchema.response?.['200']
|
|
113
|
+
modifySchema(responseSchema, modificationRules)
|
|
114
|
+
|
|
115
|
+
async function onGatewayResponse (request, reply, body) {
|
|
116
|
+
const payload = await body.json()
|
|
117
|
+
modifyPayload(payload, responseSchema, modificationRules)
|
|
118
|
+
reply.send(payload)
|
|
119
|
+
}
|
|
120
|
+
routeOptions.config.onGatewayResponse = onGatewayResponse
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
app.platformatic.addGatewayOnRouteHook(path, [method], onRouteHook)
|
|
124
|
+
app.platformatic.addComposerOnRouteHook(path, [method], onRouteHook)
|
|
125
|
+
}
|
|
126
|
+
if (Object.keys(pathSchema).length === 0) continue
|
|
127
|
+
newSchemaPaths[path] = pathSchema
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { ...schema, paths: newSchemaPaths }
|
|
131
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fp from 'fastify-plugin'
|
|
2
|
+
|
|
3
|
+
async function openApiScalarPlugin (app, opts) {
|
|
4
|
+
const { default: scalarTheme } = await import('@platformatic/scalar-theme')
|
|
5
|
+
const { default: scalarApiReference } = await import('@scalar/fastify-api-reference')
|
|
6
|
+
|
|
7
|
+
/** Serve spec file in yaml and json */
|
|
8
|
+
app.get('/documentation/json', { schema: { hide: true } }, async () => app.swagger())
|
|
9
|
+
app.get('/documentation/yaml', { schema: { hide: true } }, async () => app.swagger({ yaml: true }))
|
|
10
|
+
|
|
11
|
+
const routePrefix = opts.openapi?.swaggerPrefix || '/documentation'
|
|
12
|
+
|
|
13
|
+
await app.register(scalarApiReference, {
|
|
14
|
+
logLevel: 'warn',
|
|
15
|
+
routePrefix,
|
|
16
|
+
configuration: {
|
|
17
|
+
customCss: scalarTheme.theme
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const openApiScalar = fp(openApiScalarPlugin)
|
package/lib/proxy.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import httpProxy from '@fastify/http-proxy'
|
|
2
|
+
import { ensureLoggableError, loadModule } from '@platformatic/foundation'
|
|
3
|
+
import fp from 'fastify-plugin'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { workerData } from 'node:worker_threads'
|
|
6
|
+
import { getGlobalDispatcher } from 'undici'
|
|
7
|
+
import { initMetrics } from './metrics.js'
|
|
8
|
+
|
|
9
|
+
const kITC = Symbol.for('plt.runtime.itc')
|
|
10
|
+
const kProxyRoute = Symbol('plt.gateway.proxy.route')
|
|
11
|
+
|
|
12
|
+
const urlPattern = /^https?:\/\//
|
|
13
|
+
|
|
14
|
+
async function resolveApplicationProxyParameters (application) {
|
|
15
|
+
// Get meta information from the application, if any, to eventually hook up to a TCP port
|
|
16
|
+
const allMeta = (await globalThis[kITC]?.send('getApplicationMeta', application.id)) ?? {}
|
|
17
|
+
const meta = allMeta.gateway ?? allMeta.composer ?? { prefix: application.id }
|
|
18
|
+
|
|
19
|
+
// If no prefix could be found, assume the application id
|
|
20
|
+
let prefix = (application.proxy?.prefix ?? meta.prefix ?? application.id).replace(/(\/$)/g, '')
|
|
21
|
+
let rewritePrefix = ''
|
|
22
|
+
let internalRewriteLocationHeader = true
|
|
23
|
+
|
|
24
|
+
if (meta.wantsAbsoluteUrls) {
|
|
25
|
+
const basePath = workerData.config.basePath
|
|
26
|
+
|
|
27
|
+
// Strip the runtime basepath from the prefix when it comes from the application meta
|
|
28
|
+
if (basePath && !application.proxy?.prefix && prefix.startsWith(basePath)) {
|
|
29
|
+
prefix = prefix.substring(basePath.length)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The rewritePrefix purposely ignores application.proxy?.prefix to let
|
|
33
|
+
// the application always being able to configure their value
|
|
34
|
+
rewritePrefix = meta.prefix ?? application.id
|
|
35
|
+
internalRewriteLocationHeader = false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (application.proxy?.ws?.hooks) {
|
|
39
|
+
const hooks = await loadModule(createRequire(import.meta.filename), application.proxy.ws.hooks.path)
|
|
40
|
+
application.proxy.ws.hooks = hooks
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
origin: application.origin,
|
|
45
|
+
url: meta.url,
|
|
46
|
+
prefix,
|
|
47
|
+
rewritePrefix,
|
|
48
|
+
internalRewriteLocationHeader,
|
|
49
|
+
needsRootTrailingSlash: meta.needsRootTrailingSlash,
|
|
50
|
+
needsRefererBasedRedirect: meta.needsRefererBasedRedirect,
|
|
51
|
+
upstream: application.proxy?.upstream,
|
|
52
|
+
ws: application.proxy?.ws
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let metrics
|
|
57
|
+
|
|
58
|
+
async function proxyPlugin (app, opts) {
|
|
59
|
+
const meta = { proxies: {} }
|
|
60
|
+
const hostnameLessProxies = []
|
|
61
|
+
|
|
62
|
+
for (const application of opts.applications) {
|
|
63
|
+
if (!application.proxy) {
|
|
64
|
+
// When a application defines no expose config at all
|
|
65
|
+
// we assume a proxy exposed with a prefix equals to its id or meta.prefix
|
|
66
|
+
if (application.proxy === false || application.openapi || application.graphql) {
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const parameters = await resolveApplicationProxyParameters(application)
|
|
72
|
+
const {
|
|
73
|
+
prefix,
|
|
74
|
+
origin,
|
|
75
|
+
url,
|
|
76
|
+
rewritePrefix,
|
|
77
|
+
internalRewriteLocationHeader,
|
|
78
|
+
needsRootTrailingSlash,
|
|
79
|
+
needsRefererBasedRedirect,
|
|
80
|
+
ws
|
|
81
|
+
} = parameters
|
|
82
|
+
meta.proxies[application.id] = parameters
|
|
83
|
+
|
|
84
|
+
const basePath = `/${prefix ?? ''}`.replaceAll(/\/+/g, '/').replace(/\/$/, '')
|
|
85
|
+
const dispatcher = getGlobalDispatcher()
|
|
86
|
+
|
|
87
|
+
let preRewrite = null
|
|
88
|
+
|
|
89
|
+
if (needsRootTrailingSlash) {
|
|
90
|
+
preRewrite = function preRewrite (url) {
|
|
91
|
+
if (url === basePath) {
|
|
92
|
+
url += '/'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return url
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
Some frontends, like Astro (https://github.com/withastro/astro/issues/11445)
|
|
101
|
+
generate invalid paths in development mode which ignore the basePath.
|
|
102
|
+
In that case we try to properly redirect the browser by trying to understand the prefix
|
|
103
|
+
from the Referer header.
|
|
104
|
+
*/
|
|
105
|
+
if (needsRefererBasedRedirect) {
|
|
106
|
+
app.addHook('preHandler', function refererBasedRedirectPreHandler (req, reply, done) {
|
|
107
|
+
// If the URL is already targeted to the application, do nothing
|
|
108
|
+
if (req.url.startsWith(basePath)) {
|
|
109
|
+
done()
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Use the referer to understand the desired intent
|
|
114
|
+
const referer = req.headers.referer
|
|
115
|
+
|
|
116
|
+
if (!referer) {
|
|
117
|
+
done()
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const path = new URL(referer).pathname
|
|
122
|
+
|
|
123
|
+
// If we have a match redirect
|
|
124
|
+
if (path.startsWith(basePath)) {
|
|
125
|
+
reply.redirect(`${basePath}${req.url}`, 308)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
done()
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Do not show proxied applications in Swagger
|
|
133
|
+
if (!application.openapi) {
|
|
134
|
+
app.addHook('onRoute', routeOptions => {
|
|
135
|
+
if (routeOptions.config?.[kProxyRoute] && routeOptions.url.startsWith(basePath)) {
|
|
136
|
+
routeOptions.schema ??= {}
|
|
137
|
+
routeOptions.schema.hide = true
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const toReplace = url
|
|
143
|
+
? new RegExp(
|
|
144
|
+
url
|
|
145
|
+
.replace(/127\.0\.0\.1/, 'localhost')
|
|
146
|
+
.replace(/\[::\]/, 'localhost')
|
|
147
|
+
.replace('http://', 'https?://')
|
|
148
|
+
)
|
|
149
|
+
: null
|
|
150
|
+
|
|
151
|
+
if (!metrics) {
|
|
152
|
+
metrics = initMetrics(globalThis.platformatic?.prometheus)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const proxyOptions = {
|
|
156
|
+
prefix,
|
|
157
|
+
rewritePrefix,
|
|
158
|
+
upstream: application.proxy?.upstream ?? origin,
|
|
159
|
+
preRewrite,
|
|
160
|
+
|
|
161
|
+
websocket: true,
|
|
162
|
+
wsUpstream: ws?.upstream ?? url ?? origin,
|
|
163
|
+
wsReconnect: ws?.reconnect,
|
|
164
|
+
wsHooks: {
|
|
165
|
+
onConnect: (...args) => {
|
|
166
|
+
metrics?.activeWsConnections?.inc()
|
|
167
|
+
ws?.hooks?.onConnect(...args)
|
|
168
|
+
},
|
|
169
|
+
onDisconnect: (...args) => {
|
|
170
|
+
metrics?.activeWsConnections?.dec()
|
|
171
|
+
ws?.hooks?.onDisconnect(...args)
|
|
172
|
+
},
|
|
173
|
+
onReconnect: ws?.hooks?.onReconnect,
|
|
174
|
+
onPong: ws?.hooks?.onPong,
|
|
175
|
+
onIncomingMessage: ws?.hooks?.onIncomingMessage,
|
|
176
|
+
onOutgoingMessage: ws?.hooks?.onOutgoingMessage
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
undici: dispatcher,
|
|
180
|
+
destroyAgent: false,
|
|
181
|
+
config: {
|
|
182
|
+
[kProxyRoute]: true
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
internalRewriteLocationHeader: false,
|
|
186
|
+
replyOptions: {
|
|
187
|
+
rewriteHeaders: headers => {
|
|
188
|
+
let location = headers.location
|
|
189
|
+
if (location) {
|
|
190
|
+
if (toReplace) {
|
|
191
|
+
location = location.replace(toReplace, '')
|
|
192
|
+
}
|
|
193
|
+
if (!urlPattern.test(location) && internalRewriteLocationHeader) {
|
|
194
|
+
location = location.replace(rewritePrefix, prefix)
|
|
195
|
+
}
|
|
196
|
+
headers.location = location
|
|
197
|
+
}
|
|
198
|
+
return headers
|
|
199
|
+
},
|
|
200
|
+
rewriteRequestHeaders: (request, headers) => {
|
|
201
|
+
const targetUrl = `${origin}${request.url}`
|
|
202
|
+
const context = request.span?.context
|
|
203
|
+
const { span, telemetryHeaders } = app.openTelemetry?.startHTTPSpanClient(
|
|
204
|
+
targetUrl,
|
|
205
|
+
request.method,
|
|
206
|
+
context
|
|
207
|
+
) || { span: null, telemetryHeaders: {} }
|
|
208
|
+
// We need to store the span in a different object
|
|
209
|
+
// to correctly close it in the onResponse hook
|
|
210
|
+
// Note that we have 2 spans:
|
|
211
|
+
// - request.span: the span of the request to the proxy
|
|
212
|
+
// - request.proxedCallSpan: the span of the request to the proxied application
|
|
213
|
+
request.proxedCallSpan = span
|
|
214
|
+
|
|
215
|
+
headers = {
|
|
216
|
+
...headers,
|
|
217
|
+
...telemetryHeaders,
|
|
218
|
+
'x-forwarded-for': request.ip,
|
|
219
|
+
'x-forwarded-host': request.host,
|
|
220
|
+
'x-forwarded-proto': request.protocol
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
request.log.trace({ headers }, 'rewritten headers before proxying')
|
|
224
|
+
|
|
225
|
+
return headers
|
|
226
|
+
},
|
|
227
|
+
onResponse: (_, reply, res) => {
|
|
228
|
+
app.openTelemetry?.endHTTPSpanClient(reply.request.proxedCallSpan, {
|
|
229
|
+
statusCode: reply.statusCode,
|
|
230
|
+
headers: res.headers
|
|
231
|
+
})
|
|
232
|
+
reply.send(res.stream)
|
|
233
|
+
},
|
|
234
|
+
onError: (reply, { error }) => {
|
|
235
|
+
app.log.error({ error: ensureLoggableError(error) }, 'Error while proxying request to another application')
|
|
236
|
+
return reply.send(error)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
hostnameLessProxies.push(proxyOptions)
|
|
242
|
+
|
|
243
|
+
const host = application.proxy?.hostname
|
|
244
|
+
|
|
245
|
+
if (host) {
|
|
246
|
+
await app.register(httpProxy, {
|
|
247
|
+
...proxyOptions,
|
|
248
|
+
prefix: '/',
|
|
249
|
+
constraints: { host }
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const hostnames = opts.applications.map(s => s.proxy?.hostname).filter(Boolean)
|
|
255
|
+
for (const options of hostnameLessProxies) {
|
|
256
|
+
if (hostnames.length > 0) {
|
|
257
|
+
options.constraints = { notHost: hostnames }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await app.register(httpProxy, options)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
opts.capability?.registerMeta(meta)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export const proxy = fp(proxyPlugin)
|
package/lib/root.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fastifyStatic from '@fastify/static'
|
|
2
|
+
import fastifyView from '@fastify/view'
|
|
3
|
+
import userAgentParser from 'my-ua-parser'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import nunjucks from 'nunjucks'
|
|
6
|
+
|
|
7
|
+
export default function root (app) {
|
|
8
|
+
app.register(fastifyStatic, {
|
|
9
|
+
root: join(import.meta.dirname, '../public')
|
|
10
|
+
})
|
|
11
|
+
app.register(fastifyView, {
|
|
12
|
+
engine: {
|
|
13
|
+
nunjucks
|
|
14
|
+
},
|
|
15
|
+
root: join(import.meta.dirname, '../public')
|
|
16
|
+
})
|
|
17
|
+
// root endpoint
|
|
18
|
+
app.route({
|
|
19
|
+
method: 'GET',
|
|
20
|
+
path: '/',
|
|
21
|
+
schema: { hide: true },
|
|
22
|
+
handler: async (req, reply) => {
|
|
23
|
+
const uaString = req.headers['user-agent']
|
|
24
|
+
let hasOpenAPIServices = false
|
|
25
|
+
let hasGraphQLServices = false
|
|
26
|
+
if (uaString) {
|
|
27
|
+
const parsed = userAgentParser(uaString)
|
|
28
|
+
if (parsed.browser.name !== undefined) {
|
|
29
|
+
const serviceTypes = {
|
|
30
|
+
proxy: {
|
|
31
|
+
title: 'Reverse Proxy',
|
|
32
|
+
icon: './images/reverse-proxy.svg',
|
|
33
|
+
services: []
|
|
34
|
+
},
|
|
35
|
+
openapi: {
|
|
36
|
+
title: 'OpenAPI',
|
|
37
|
+
icon: './images/openapi.svg',
|
|
38
|
+
services: []
|
|
39
|
+
},
|
|
40
|
+
graphql: {
|
|
41
|
+
title: 'GraphQL',
|
|
42
|
+
icon: './images/graphql.svg',
|
|
43
|
+
services: []
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
app.platformatic.config.gateway.applications.forEach(s => {
|
|
48
|
+
if (s.openapi) {
|
|
49
|
+
hasOpenAPIServices = true
|
|
50
|
+
serviceTypes.openapi.services.push(s)
|
|
51
|
+
}
|
|
52
|
+
if (s.graphql) {
|
|
53
|
+
hasGraphQLServices = true
|
|
54
|
+
serviceTypes.graphql.services.push(s)
|
|
55
|
+
}
|
|
56
|
+
if (s.proxy) {
|
|
57
|
+
serviceTypes.proxy.services.push({
|
|
58
|
+
...s,
|
|
59
|
+
externalLink: `${s.proxy.prefix}/`
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return reply.view('index.njk', {
|
|
65
|
+
hasGraphQLServices,
|
|
66
|
+
hasOpenAPIServices,
|
|
67
|
+
services: serviceTypes
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Load services
|
|
72
|
+
return { message: 'Welcome to Platformatic! Please visit https://docs.platformatic.dev' }
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
}
|