-
Notifications
You must be signed in to change notification settings - Fork 2
/
server.ts
183 lines (165 loc) · 4.84 KB
/
server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import util from 'util'
import { MeshHTTPHandler } from '@graphql-mesh/http'
import { createClient } from '@supabase/supabase-js'
import Fastify, { FastifyRequest } from 'fastify'
import * as jsonwebtoken from 'jsonwebtoken'
import * as winston from 'winston'
import { createBuiltMeshHTTPHandler } from '../.mesh/index'
const app = Fastify({
trustProxy: true,
logger: process.env.NODE_ENV !== 'production'
})
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const meshHttp: MeshHTTPHandler = createBuiltMeshHTTPHandler()
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY,
{
auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false
}
}
)
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [new winston.transports.Console()]
})
const processAuthorizationHeader = async (req: FastifyRequest) => {
// read jwt from Authorization Bearer header
const authorizationHeader = req.headers.authorization
if (
typeof authorizationHeader !== 'string' ||
!authorizationHeader.startsWith('Bearer') ||
authorizationHeader.length < 7
) {
logger.info('Authorization header is not a valid Bearer token')
throw Error('Authorization header is not a valid Bearer token')
}
const jwt = authorizationHeader?.substring(7)
if (!jwt) {
logger.warn('No jwt')
throw Error('No jwt')
}
// Get user from Supabase if JWT is valid
const {
data: { user },
error
} = await supabase.auth.getUser(jwt)
if (error) {
logger.error('Error when getting user by jwt', error)
throw Error('Error when getting user')
}
if (!user) {
logger.error("Couldn't get user", error)
throw Error("Couldn't get user")
}
req.headers.authorization = `Bearer ${jwt}`
req.headers.authusersid = user.id
}
const processUserApiKeyHeader = async (req: FastifyRequest) => {
if (typeof req.headers.userapikey !== 'string') {
logger.info('User API Key is no string or empty')
throw Error('User API Key is no string or empty')
}
if (req.headers.userapikey.trim() === '') {
logger.info('User API Key is empty')
throw Error('User API Key is empty')
}
const userApiKey = req.headers.userapikey
const { data, error: selectError } = await supabase
.from('user_api_key')
.select('auth_users_id')
.eq('user_api_key', userApiKey)
if (selectError) {
logger.info('Error when selecting userapikey from db', selectError)
throw Error('Error when selecting userapikey from db')
}
if (!data?.[0]?.auth_users_id) {
logger.warn("User ID couldn't be retrieved from db for userapikey")
throw Error("User ID couldn't be retrieved from db for userapikey")
}
const userId = data?.[0]?.auth_users_id as string
const {
data: { user },
error: userByIdError
} = await supabase.auth.admin.getUserById(userId)
if (userByIdError) {
logger.error(
'Error when getting User by ID',
userId,
", User doesn't seem to exist",
userByIdError
)
throw Error('Error when getting User by ID')
}
if (!user) {
logger.warn('User not found')
throw Error('User not found')
}
const jwt = jsonwebtoken.sign(
{
sub: user?.id,
iss: `${process.env.SUPABASE_URL}/auth/v1`,
aud: user.aud,
role: user.role,
email: user.email,
app_metadata: user.app_metadata,
user_metadata: user.user_metadata
},
process.env.SUPABASE_JWT_SECRET,
{ expiresIn: '1h' }
)
req.headers.authorization = `Bearer ${jwt}`
req.headers.authusersid = userId
}
app.addHook('preHandler', async (req) => {
// if Authorization header exists
if (req?.headers?.authorization) {
await processAuthorizationHeader(req)
}
// if userApiKey header exists
if (req?.headers?.userapikey) {
await processUserApiKeyHeader(req)
}
})
app.route({
url: '/api/graphql',
method: ['GET', 'POST', 'OPTIONS'],
async handler(req, reply) {
console.log('request debug', util.inspect(req))
// Second parameter adds Fastify's `req` and `reply` to the GraphQL Context
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const response = await meshHttp.handleNodeRequest(req, {
req,
reply
})
console.log('response debug', util.inspect(response))
// biome-ignore lint/complexity/noForEach: type is an interface with forEach method
response.headers.forEach((value: unknown, key: string) => {
void reply.header(key, value)
})
void reply.status(response.status)
const reader = response?.body?.getReader()
while (reader) {
const { done, value } = await reader.read()
if (done) break
void reply.send(value)
}
return reply
}
})
void app.listen(
{ port: process.env.GRAPHQL_GATEWAY_PORT, host: '0.0.0.0' },
(err, address) => {
if (err) {
app.log.error(err)
process.exit(1)
}
app.log.info(`server listening on ${address}`)
}
)