mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2026-03-01 12:44:37 +00:00
148 lines
5.0 KiB
TypeScript
148 lines
5.0 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
/* eslint-disable import-x/no-named-as-default-member */
|
|
/* eslint-disable import-x/no-deprecated */
|
|
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
|
import http from 'node:http'
|
|
|
|
import { ApolloServer } from '@apollo/server'
|
|
import { expressMiddleware } from '@apollo/server/express4'
|
|
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'
|
|
import bodyParser from 'body-parser'
|
|
import express from 'express'
|
|
import { execute, subscribe } from 'graphql'
|
|
import { graphqlUploadExpress } from 'graphql-upload'
|
|
import { useServer } from 'graphql-ws/lib/use/ws'
|
|
import helmet from 'helmet'
|
|
import { SubscriptionServer } from 'subscriptions-transport-ws'
|
|
import { WebSocketServer } from 'ws'
|
|
|
|
import CONFIG from './config'
|
|
import { getContext } from './context'
|
|
import schema from './graphql/schema'
|
|
import logger from './logger'
|
|
import middleware from './middleware'
|
|
|
|
import type { ApolloServerPlugin } from '@apollo/server'
|
|
|
|
interface CreateServerOptions {
|
|
context?: (req: { headers: { authorization?: string } }) => Promise<any>
|
|
plugins?: ApolloServerPlugin[]
|
|
}
|
|
|
|
const createServer = async (options?: CreateServerOptions) => {
|
|
const app = express()
|
|
const httpServer = http.createServer(app)
|
|
const appliedSchema = middleware(schema)
|
|
|
|
// Two WebSocket servers for dual protocol support (noServer mode)
|
|
const wsServer = new WebSocketServer({ noServer: true })
|
|
const legacyWsServer = new WebSocketServer({ noServer: true })
|
|
|
|
// New protocol: graphql-ws (subprotocol: graphql-transport-ws)
|
|
const serverCleanup = useServer(
|
|
{
|
|
schema: appliedSchema,
|
|
context: async (ctx) =>
|
|
getContext()(ctx.connectionParams as { headers: { authorization?: string } }),
|
|
onDisconnect: () => {
|
|
logger.debug('WebSocket client disconnected')
|
|
},
|
|
},
|
|
wsServer,
|
|
)
|
|
|
|
// Legacy protocol: subscriptions-transport-ws (subprotocol: graphql-ws)
|
|
const legacyServerCleanup = SubscriptionServer.create(
|
|
{
|
|
schema: appliedSchema,
|
|
execute,
|
|
subscribe,
|
|
onConnect: async (connectionParams: Record<string, unknown>) => {
|
|
return getContext()(connectionParams as { headers: { authorization?: string } })
|
|
},
|
|
onDisconnect: () => {
|
|
logger.debug('Legacy WebSocket client disconnected')
|
|
},
|
|
},
|
|
legacyWsServer,
|
|
)
|
|
|
|
// Route WebSocket upgrade requests based on subprotocol
|
|
httpServer.on('upgrade', (req, socket, head) => {
|
|
const protocol = req.headers['sec-websocket-protocol']
|
|
const isLegacy = protocol === 'graphql-ws' || !protocol
|
|
const targetServer = isLegacy ? legacyWsServer : wsServer
|
|
targetServer.handleUpgrade(req, socket, head, (ws) => {
|
|
targetServer.emit('connection', ws, req)
|
|
})
|
|
})
|
|
|
|
const server = new ApolloServer({
|
|
schema: appliedSchema,
|
|
// TODO: Re-enable CSRF prevention once the webapp sends the 'apollo-require-preflight' header.
|
|
// Currently disabled because the Nuxt 2 webapp uses apollo-upload-client for multipart/form-data
|
|
// file uploads, which Apollo Server 4 blocks by default as a CSRF vector. The webapp relies on
|
|
// JWT/cookie authentication and CORS configuration for request validation instead.
|
|
csrfPrevention: false,
|
|
formatError: (formattedError, error) => {
|
|
if (formattedError.message === 'ERROR_VALIDATION') {
|
|
return {
|
|
...formattedError,
|
|
message: String(
|
|
(error as any).originalError?.details?.map((d) => d.message) ?? formattedError.message,
|
|
),
|
|
}
|
|
}
|
|
return formattedError
|
|
},
|
|
plugins: [
|
|
ApolloServerPluginDrainHttpServer({ httpServer }),
|
|
{
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
async serverWillStart() {
|
|
return {
|
|
async drainServer() {
|
|
await serverCleanup.dispose()
|
|
legacyServerCleanup.close()
|
|
},
|
|
}
|
|
},
|
|
},
|
|
...(options?.plugins ?? []),
|
|
],
|
|
})
|
|
|
|
await server.start()
|
|
|
|
// TODO: this exception is required for the graphql playground, since the playground loads external resources
|
|
// See: https://github.com/graphql/graphql-playground/issues/1283
|
|
app.use(
|
|
helmet(
|
|
(CONFIG.DEBUG && { contentSecurityPolicy: false, crossOriginEmbedderPolicy: false }) || {},
|
|
) as any,
|
|
)
|
|
app.use(express.static('public'))
|
|
app.use(graphqlUploadExpress())
|
|
app.use(
|
|
'/',
|
|
bodyParser.json({ limit: '10mb' }) as any,
|
|
bodyParser.urlencoded({ limit: '10mb', extended: true }) as any,
|
|
expressMiddleware(server, {
|
|
context: async ({ req }) => {
|
|
if (options?.context) {
|
|
return options.context(req)
|
|
}
|
|
return getContext()(req)
|
|
},
|
|
}),
|
|
)
|
|
|
|
return { server, httpServer, app }
|
|
}
|
|
|
|
export default createServer
|