wabe 0.6.9 → 0.6.10
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/README.md +138 -32
- package/bucket/b.txt +1 -0
- package/dev/index.ts +215 -0
- package/dist/authentication/Session.d.ts +4 -1
- package/dist/authentication/interface.d.ts +16 -0
- package/dist/email/interface.d.ts +1 -1
- package/dist/graphql/resolvers.d.ts +4 -2
- package/dist/hooks/index.d.ts +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +8713 -8867
- package/dist/server/index.d.ts +4 -2
- package/dist/utils/crypto.d.ts +7 -0
- package/dist/utils/helper.d.ts +4 -1
- package/generated/schema.graphql +16 -14
- package/generated/wabe.ts +4 -4
- package/package.json +15 -15
- package/src/authentication/OTP.test.ts +69 -0
- package/src/authentication/OTP.ts +66 -0
- package/src/authentication/Session.test.ts +665 -0
- package/src/authentication/Session.ts +529 -0
- package/src/authentication/defaultAuthentication.ts +214 -0
- package/src/authentication/index.ts +3 -0
- package/src/authentication/interface.ts +157 -0
- package/src/authentication/oauth/GitHub.test.ts +105 -0
- package/src/authentication/oauth/GitHub.ts +133 -0
- package/src/authentication/oauth/Google.test.ts +105 -0
- package/src/authentication/oauth/Google.ts +110 -0
- package/src/authentication/oauth/Oauth2Client.test.ts +225 -0
- package/src/authentication/oauth/Oauth2Client.ts +140 -0
- package/src/authentication/oauth/index.ts +2 -0
- package/src/authentication/oauth/utils.test.ts +35 -0
- package/src/authentication/oauth/utils.ts +28 -0
- package/src/authentication/providers/EmailOTP.test.ts +138 -0
- package/src/authentication/providers/EmailOTP.ts +93 -0
- package/src/authentication/providers/EmailPassword.test.ts +187 -0
- package/src/authentication/providers/EmailPassword.ts +130 -0
- package/src/authentication/providers/EmailPasswordSRP.test.ts +206 -0
- package/src/authentication/providers/EmailPasswordSRP.ts +184 -0
- package/src/authentication/providers/GitHub.ts +30 -0
- package/src/authentication/providers/Google.ts +30 -0
- package/src/authentication/providers/OAuth.test.ts +185 -0
- package/src/authentication/providers/OAuth.ts +112 -0
- package/src/authentication/providers/PhonePassword.test.ts +187 -0
- package/src/authentication/providers/PhonePassword.ts +129 -0
- package/src/authentication/providers/QRCodeOTP.test.ts +79 -0
- package/src/authentication/providers/QRCodeOTP.ts +65 -0
- package/src/authentication/providers/index.ts +6 -0
- package/src/authentication/resolvers/refreshResolver.test.ts +37 -0
- package/src/authentication/resolvers/refreshResolver.ts +20 -0
- package/src/authentication/resolvers/signInWithResolver.inte.test.ts +59 -0
- package/src/authentication/resolvers/signInWithResolver.test.ts +307 -0
- package/src/authentication/resolvers/signInWithResolver.ts +102 -0
- package/src/authentication/resolvers/signOutResolver.test.ts +41 -0
- package/src/authentication/resolvers/signOutResolver.ts +22 -0
- package/src/authentication/resolvers/signUpWithResolver.test.ts +186 -0
- package/src/authentication/resolvers/signUpWithResolver.ts +69 -0
- package/src/authentication/resolvers/verifyChallenge.test.ts +136 -0
- package/src/authentication/resolvers/verifyChallenge.ts +69 -0
- package/src/authentication/roles.test.ts +59 -0
- package/src/authentication/roles.ts +40 -0
- package/src/authentication/utils.test.ts +99 -0
- package/src/authentication/utils.ts +43 -0
- package/src/cache/InMemoryCache.test.ts +62 -0
- package/src/cache/InMemoryCache.ts +45 -0
- package/src/cron/index.test.ts +17 -0
- package/src/cron/index.ts +46 -0
- package/src/database/DatabaseController.test.ts +625 -0
- package/src/database/DatabaseController.ts +983 -0
- package/src/database/index.test.ts +1230 -0
- package/src/database/index.ts +9 -0
- package/src/database/interface.ts +312 -0
- package/src/email/DevAdapter.ts +8 -0
- package/src/email/EmailController.test.ts +29 -0
- package/src/email/EmailController.ts +13 -0
- package/src/email/index.ts +2 -0
- package/src/email/interface.ts +36 -0
- package/src/email/templates/sendOtpCode.ts +120 -0
- package/src/file/FileController.ts +28 -0
- package/src/file/FileDevAdapter.ts +54 -0
- package/src/file/hookDeleteFile.ts +27 -0
- package/src/file/hookReadFile.ts +70 -0
- package/src/file/hookUploadFile.ts +53 -0
- package/src/file/index.test.ts +979 -0
- package/src/file/index.ts +2 -0
- package/src/file/interface.ts +42 -0
- package/src/graphql/GraphQLSchema.test.ts +4399 -0
- package/src/graphql/GraphQLSchema.ts +928 -0
- package/src/graphql/index.ts +2 -0
- package/src/graphql/parseGraphqlSchema.ts +94 -0
- package/src/graphql/parser.test.ts +217 -0
- package/src/graphql/parser.ts +566 -0
- package/src/graphql/pointerAndRelationFunction.ts +200 -0
- package/src/graphql/resolvers.ts +467 -0
- package/src/graphql/tests/aggregation.test.ts +1123 -0
- package/src/graphql/tests/e2e.test.ts +596 -0
- package/src/graphql/tests/scalars.test.ts +250 -0
- package/src/graphql/types.ts +219 -0
- package/src/hooks/HookObject.test.ts +122 -0
- package/src/hooks/HookObject.ts +168 -0
- package/src/hooks/authentication.ts +76 -0
- package/src/hooks/createUser.test.ts +77 -0
- package/src/hooks/createUser.ts +10 -0
- package/src/hooks/defaultFields.test.ts +187 -0
- package/src/hooks/defaultFields.ts +40 -0
- package/src/hooks/deleteSession.test.ts +181 -0
- package/src/hooks/deleteSession.ts +20 -0
- package/src/hooks/hashFieldHook.test.ts +163 -0
- package/src/hooks/hashFieldHook.ts +97 -0
- package/src/hooks/index.test.ts +207 -0
- package/src/hooks/index.ts +430 -0
- package/src/hooks/permissions.test.ts +424 -0
- package/src/hooks/permissions.ts +113 -0
- package/src/hooks/protected.test.ts +551 -0
- package/src/hooks/protected.ts +72 -0
- package/src/hooks/searchableFields.test.ts +166 -0
- package/src/hooks/searchableFields.ts +98 -0
- package/src/hooks/session.test.ts +138 -0
- package/src/hooks/session.ts +78 -0
- package/src/hooks/setEmail.test.ts +216 -0
- package/src/hooks/setEmail.ts +35 -0
- package/src/hooks/setupAcl.test.ts +589 -0
- package/src/hooks/setupAcl.ts +29 -0
- package/src/index.ts +9 -0
- package/src/schema/Schema.test.ts +484 -0
- package/src/schema/Schema.ts +795 -0
- package/src/schema/defaultResolvers.ts +94 -0
- package/src/schema/index.ts +1 -0
- package/src/schema/resolvers/meResolver.test.ts +62 -0
- package/src/schema/resolvers/meResolver.ts +14 -0
- package/src/schema/resolvers/newFile.ts +0 -0
- package/src/schema/resolvers/resetPassword.test.ts +345 -0
- package/src/schema/resolvers/resetPassword.ts +64 -0
- package/src/schema/resolvers/sendEmail.test.ts +118 -0
- package/src/schema/resolvers/sendEmail.ts +21 -0
- package/src/schema/resolvers/sendOtpCode.test.ts +153 -0
- package/src/schema/resolvers/sendOtpCode.ts +52 -0
- package/src/security.test.ts +3461 -0
- package/src/server/defaultSessionHandler.test.ts +66 -0
- package/src/server/defaultSessionHandler.ts +115 -0
- package/src/server/generateCodegen.ts +476 -0
- package/src/server/index.test.ts +552 -0
- package/src/server/index.ts +354 -0
- package/src/server/interface.ts +11 -0
- package/src/server/routes/authHandler.ts +187 -0
- package/src/server/routes/index.ts +40 -0
- package/src/utils/crypto.test.ts +41 -0
- package/src/utils/crypto.ts +121 -0
- package/src/utils/export.ts +13 -0
- package/src/utils/helper.ts +195 -0
- package/src/utils/index.test.ts +11 -0
- package/src/utils/index.ts +201 -0
- package/src/utils/preload.ts +8 -0
- package/src/utils/testHelper.ts +117 -0
- package/tsconfig.json +32 -0
- package/bunfig.toml +0 -4
- package/dist/ai/index.d.ts +0 -1
- package/dist/ai/interface.d.ts +0 -9
- /package/dist/server/{defaultHandlers.d.ts → defaultSessionHandler.d.ts} +0 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import type { DatabaseConfig } from '../database'
|
|
2
|
+
import { DatabaseController } from '../database/DatabaseController'
|
|
3
|
+
import {
|
|
4
|
+
type EnumInterface,
|
|
5
|
+
Schema,
|
|
6
|
+
type SchemaInterface,
|
|
7
|
+
} from '../schema/Schema'
|
|
8
|
+
import { GraphQLError, GraphQLObjectType, GraphQLSchema } from 'graphql'
|
|
9
|
+
import { GraphQLSchema as WabeGraphQLSchema } from '../graphql'
|
|
10
|
+
import type { AuthenticationConfig } from '../authentication/interface'
|
|
11
|
+
import { type WabeRoute, defaultRoutes } from './routes'
|
|
12
|
+
import { type Hook, getDefaultHooks } from '../hooks'
|
|
13
|
+
import { generateCodegen } from './generateCodegen'
|
|
14
|
+
import { defaultAuthenticationMethods } from '../authentication/defaultAuthentication'
|
|
15
|
+
import { Wobe, cors, rateLimit } from 'wobe'
|
|
16
|
+
import type { Context, CorsOptions, RateLimitOptions } from 'wobe'
|
|
17
|
+
import type { WabeContext } from './interface'
|
|
18
|
+
import { initializeRoles } from '../authentication/roles'
|
|
19
|
+
import type { EmailConfig } from '../email'
|
|
20
|
+
import { EmailController } from '../email/EmailController'
|
|
21
|
+
import { FileController } from '../file/FileController'
|
|
22
|
+
import { defaultSessionHandler } from './defaultSessionHandler'
|
|
23
|
+
import type { CronConfig } from '../cron'
|
|
24
|
+
import type { FileConfig } from '../file'
|
|
25
|
+
import { WobeGraphqlYogaPlugin } from 'wobe-graphql-yoga'
|
|
26
|
+
|
|
27
|
+
type SecurityConfig = {
|
|
28
|
+
corsOptions?: CorsOptions
|
|
29
|
+
rateLimit?: RateLimitOptions
|
|
30
|
+
hideSensitiveErrorMessage?: boolean
|
|
31
|
+
disableCSRFProtection?: boolean
|
|
32
|
+
allowIntrospectionInProduction?: boolean
|
|
33
|
+
maxGraphqlDepth?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export * from './interface'
|
|
37
|
+
export * from './routes'
|
|
38
|
+
|
|
39
|
+
export const defaultRoles = ['DashboardAdmin']
|
|
40
|
+
|
|
41
|
+
export interface WabeConfig<T extends WabeTypes> {
|
|
42
|
+
port: number
|
|
43
|
+
isProduction: boolean
|
|
44
|
+
hostname?: string
|
|
45
|
+
security?: SecurityConfig
|
|
46
|
+
schema?: SchemaInterface<T>
|
|
47
|
+
graphqlSchema?: GraphQLSchema
|
|
48
|
+
database: DatabaseConfig<T>
|
|
49
|
+
codegen?:
|
|
50
|
+
| {
|
|
51
|
+
enabled: true
|
|
52
|
+
path: string
|
|
53
|
+
}
|
|
54
|
+
| { enabled?: false }
|
|
55
|
+
authentication?: AuthenticationConfig<T>
|
|
56
|
+
routes?: WabeRoute[]
|
|
57
|
+
rootKey: string
|
|
58
|
+
hooks?: Hook<T, any>[]
|
|
59
|
+
email?: EmailConfig
|
|
60
|
+
file?: FileConfig<T>
|
|
61
|
+
crons?: CronConfig<T>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type WabeTypes = {
|
|
65
|
+
types: Record<any, any>
|
|
66
|
+
where: Record<any, any>
|
|
67
|
+
scalars: string
|
|
68
|
+
enums: Record<any, any>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type WobeCustomContext<T extends WabeTypes> = Context & {
|
|
72
|
+
wabe: WabeContext<T>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type WabeControllers<T extends WabeTypes> = {
|
|
76
|
+
database: DatabaseController<T>
|
|
77
|
+
email?: EmailController
|
|
78
|
+
file?: FileController
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class Wabe<T extends WabeTypes> {
|
|
82
|
+
public server: Wobe<WobeCustomContext<T>>
|
|
83
|
+
|
|
84
|
+
public config: WabeConfig<T>
|
|
85
|
+
public controllers: WabeControllers<T>
|
|
86
|
+
|
|
87
|
+
constructor({
|
|
88
|
+
isProduction,
|
|
89
|
+
port,
|
|
90
|
+
hostname,
|
|
91
|
+
security,
|
|
92
|
+
schema,
|
|
93
|
+
database,
|
|
94
|
+
authentication,
|
|
95
|
+
rootKey,
|
|
96
|
+
codegen,
|
|
97
|
+
hooks,
|
|
98
|
+
file,
|
|
99
|
+
email,
|
|
100
|
+
routes,
|
|
101
|
+
crons,
|
|
102
|
+
}: WabeConfig<T>) {
|
|
103
|
+
this.config = {
|
|
104
|
+
isProduction,
|
|
105
|
+
port,
|
|
106
|
+
hostname,
|
|
107
|
+
security,
|
|
108
|
+
schema,
|
|
109
|
+
database,
|
|
110
|
+
codegen,
|
|
111
|
+
authentication,
|
|
112
|
+
rootKey,
|
|
113
|
+
hooks,
|
|
114
|
+
email,
|
|
115
|
+
routes,
|
|
116
|
+
file,
|
|
117
|
+
crons,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.server = new Wobe<WobeCustomContext<T>>({ hostname }).get(
|
|
121
|
+
'/health',
|
|
122
|
+
(context) => {
|
|
123
|
+
context.res.status = 200
|
|
124
|
+
context.res.send('OK')
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
this.controllers = {
|
|
129
|
+
database: new DatabaseController<T>(database.adapter),
|
|
130
|
+
email: email?.adapter ? new EmailController(email.adapter) : undefined,
|
|
131
|
+
file: file?.adapter ? new FileController(file.adapter, this) : undefined,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.loadCrons()
|
|
135
|
+
this.loadAuthenticationMethods()
|
|
136
|
+
this.loadRoleEnum()
|
|
137
|
+
this.loadRoutes()
|
|
138
|
+
this.loadHooks()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
loadCrons() {
|
|
142
|
+
if (!this.config.crons) return
|
|
143
|
+
|
|
144
|
+
const crons = this.config.crons.map((cron) => ({
|
|
145
|
+
...cron,
|
|
146
|
+
job: cron.cron(this),
|
|
147
|
+
}))
|
|
148
|
+
|
|
149
|
+
this.config.crons = crons
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
loadRoleEnum() {
|
|
153
|
+
const roles = [
|
|
154
|
+
...defaultRoles,
|
|
155
|
+
...(this.config.authentication?.roles || []),
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
const roleEnum: EnumInterface = {
|
|
159
|
+
name: 'RoleEnum',
|
|
160
|
+
values: roles.reduce(
|
|
161
|
+
(acc, currentRole) => {
|
|
162
|
+
acc[currentRole] = currentRole
|
|
163
|
+
return acc
|
|
164
|
+
},
|
|
165
|
+
{} as Record<string, any>,
|
|
166
|
+
),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.config.schema = {
|
|
170
|
+
...this.config.schema,
|
|
171
|
+
enums: [...(this.config.schema?.enums || []), roleEnum],
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
loadAuthenticationMethods() {
|
|
176
|
+
this.config.authentication = {
|
|
177
|
+
...this.config.authentication,
|
|
178
|
+
customAuthenticationMethods: [
|
|
179
|
+
...defaultAuthenticationMethods<T>(),
|
|
180
|
+
...(this.config.authentication?.customAuthenticationMethods || []),
|
|
181
|
+
],
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
loadHooks() {
|
|
186
|
+
if (this.config.hooks?.find((hook) => hook.priority <= 0))
|
|
187
|
+
throw new Error('Hook priority <= 0 is reserved for internal uses')
|
|
188
|
+
|
|
189
|
+
this.config.hooks = [...getDefaultHooks(), ...(this.config.hooks || [])]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
loadRoutes() {
|
|
193
|
+
const wabeRoutes = [
|
|
194
|
+
...defaultRoutes(
|
|
195
|
+
this.config.file?.devDirectory || `${__dirname}/../../bucket`,
|
|
196
|
+
),
|
|
197
|
+
...(this.config.routes || []),
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
wabeRoutes.forEach((route) => {
|
|
201
|
+
const { method } = route
|
|
202
|
+
|
|
203
|
+
switch (method) {
|
|
204
|
+
case 'GET':
|
|
205
|
+
this.server.get(route.path, route.handler)
|
|
206
|
+
break
|
|
207
|
+
case 'POST':
|
|
208
|
+
this.server.post(route.path, route.handler)
|
|
209
|
+
break
|
|
210
|
+
case 'PUT':
|
|
211
|
+
this.server.put(route.path, route.handler)
|
|
212
|
+
break
|
|
213
|
+
case 'DELETE':
|
|
214
|
+
this.server.delete(route.path, route.handler)
|
|
215
|
+
break
|
|
216
|
+
default:
|
|
217
|
+
throw new Error('Invalid method for default route')
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async start() {
|
|
223
|
+
if (
|
|
224
|
+
this.config.authentication?.session &&
|
|
225
|
+
!this.config.authentication.session.jwtSecret
|
|
226
|
+
)
|
|
227
|
+
throw new Error('Authentication session requires jwt secret')
|
|
228
|
+
|
|
229
|
+
const wabeSchema = new Schema(this.config)
|
|
230
|
+
|
|
231
|
+
this.config.schema = wabeSchema.schema
|
|
232
|
+
|
|
233
|
+
await this.controllers.database.initializeDatabase(wabeSchema.schema)
|
|
234
|
+
|
|
235
|
+
const graphqlSchema = new WabeGraphQLSchema(wabeSchema)
|
|
236
|
+
|
|
237
|
+
const types = graphqlSchema.createSchema()
|
|
238
|
+
|
|
239
|
+
this.config.graphqlSchema = new GraphQLSchema({
|
|
240
|
+
query: new GraphQLObjectType({
|
|
241
|
+
name: 'Query',
|
|
242
|
+
fields: types.queries,
|
|
243
|
+
}),
|
|
244
|
+
mutation: new GraphQLObjectType({
|
|
245
|
+
name: 'Mutation',
|
|
246
|
+
fields: types.mutations,
|
|
247
|
+
}),
|
|
248
|
+
types: [...types.scalars, ...types.enums, ...types.objects],
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
if (
|
|
252
|
+
!this.config.isProduction &&
|
|
253
|
+
process.env.NODE_ENV !== 'test' &&
|
|
254
|
+
this.config.codegen &&
|
|
255
|
+
this.config.codegen.enabled &&
|
|
256
|
+
this.config.codegen.path.length > 0
|
|
257
|
+
) {
|
|
258
|
+
await generateCodegen({
|
|
259
|
+
path: this.config.codegen.path,
|
|
260
|
+
schema: wabeSchema.schema,
|
|
261
|
+
graphqlSchema: this.config.graphqlSchema,
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
// If we just want codegen we exit before server created.
|
|
265
|
+
// Not the best solution but useful to avoid multiple source of truth
|
|
266
|
+
if (process.env.CODEGEN) process.exit(0)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.server.options(
|
|
270
|
+
'/*',
|
|
271
|
+
(ctx) => {
|
|
272
|
+
return ctx.res.send('OK')
|
|
273
|
+
},
|
|
274
|
+
cors(this.config.security?.corsOptions),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
const rateLimitOptions = this.config.security?.rateLimit
|
|
278
|
+
|
|
279
|
+
if (rateLimitOptions) this.server.beforeHandler(rateLimit(rateLimitOptions))
|
|
280
|
+
|
|
281
|
+
this.server.beforeHandler(cors(this.config.security?.corsOptions))
|
|
282
|
+
|
|
283
|
+
// Set the wabe context
|
|
284
|
+
this.server.beforeHandler(
|
|
285
|
+
// @ts-expect-error
|
|
286
|
+
this.config.authentication?.sessionHandler ||
|
|
287
|
+
// @ts-expect-error
|
|
288
|
+
defaultSessionHandler(this),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
const maxDepth = this.config.security?.maxGraphqlDepth ?? 50
|
|
292
|
+
|
|
293
|
+
await this.server.usePlugin(
|
|
294
|
+
WobeGraphqlYogaPlugin({
|
|
295
|
+
schema: this.config.graphqlSchema,
|
|
296
|
+
maskedErrors:
|
|
297
|
+
this.config.security?.hideSensitiveErrorMessage ||
|
|
298
|
+
this.config.isProduction,
|
|
299
|
+
allowIntrospection:
|
|
300
|
+
!!this.config.security?.allowIntrospectionInProduction,
|
|
301
|
+
maxDepth,
|
|
302
|
+
allowMultipleOperations: true,
|
|
303
|
+
graphqlEndpoint: '/graphql',
|
|
304
|
+
plugins: [
|
|
305
|
+
{
|
|
306
|
+
// @ts-expect-error
|
|
307
|
+
onValidate: ({ addValidationRule }) => {
|
|
308
|
+
// @ts-expect-error
|
|
309
|
+
addValidationRule((context) => {
|
|
310
|
+
return {
|
|
311
|
+
// @ts-expect-error
|
|
312
|
+
Field(node) {
|
|
313
|
+
const introspectionFields = [
|
|
314
|
+
'__schema',
|
|
315
|
+
'__type',
|
|
316
|
+
'__typeKind',
|
|
317
|
+
'__field',
|
|
318
|
+
'__inputValue',
|
|
319
|
+
'__enumValue',
|
|
320
|
+
'__directive',
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
if (introspectionFields.includes(node.name.value)) {
|
|
324
|
+
context.reportError(
|
|
325
|
+
new GraphQLError(
|
|
326
|
+
'GraphQL introspection is not allowed in production',
|
|
327
|
+
),
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
context: async (ctx): Promise<WabeContext<T>> => ctx.wabe,
|
|
337
|
+
}),
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
// @ts-expect-error
|
|
341
|
+
await Promise.all([initializeRoles(this)])
|
|
342
|
+
|
|
343
|
+
this.server.listen(this.config.port, ({ port }) => {
|
|
344
|
+
if (!process.env.TEST) console.log(`Server is running on port ${port}`)
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async close() {
|
|
349
|
+
await this.controllers.database.close()
|
|
350
|
+
this.server.stop()
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export { generateCodegen }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WobeResponse } from 'wobe'
|
|
2
|
+
import type { Wabe, WabeTypes } from '.'
|
|
3
|
+
|
|
4
|
+
export interface WabeContext<T extends WabeTypes> {
|
|
5
|
+
response?: WobeResponse
|
|
6
|
+
user?: T['types']['User'] | null
|
|
7
|
+
sessionId?: string | null
|
|
8
|
+
isRoot: boolean
|
|
9
|
+
wabe: Wabe<T>
|
|
10
|
+
isGraphQLCall?: boolean
|
|
11
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { Context } from 'wobe'
|
|
2
|
+
import type { WabeContext } from '../interface'
|
|
3
|
+
import { ProviderEnum } from '../../authentication/interface'
|
|
4
|
+
import { getGraphqlClient } from '../../utils/helper'
|
|
5
|
+
import { gql } from 'graphql-request'
|
|
6
|
+
import { Google } from '../../authentication/oauth'
|
|
7
|
+
import { generateRandomValues } from '../../authentication/oauth/utils'
|
|
8
|
+
import { GitHub } from '../../authentication/oauth/GitHub'
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
- Generate code verifier (back)
|
|
12
|
+
- Sent post request to a route on back with code verifier in url (back)
|
|
13
|
+
- Generate code challenge (back)
|
|
14
|
+
- Redirect the user to google auth page with code challenge (back -> front)
|
|
15
|
+
- User sign in with google (front)
|
|
16
|
+
- The user is redirected to the route with the code (front -> back)
|
|
17
|
+
- Get the code from the url (back)
|
|
18
|
+
- Validate and sign in with google provider (back)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// https://www.rfc-editor.org/rfc/rfc7636#section-4.4 not precise the storage of codeVerifier
|
|
22
|
+
export const oauthHandlerCallback = async (
|
|
23
|
+
context: Context,
|
|
24
|
+
wabeContext: WabeContext<any>,
|
|
25
|
+
) => {
|
|
26
|
+
try {
|
|
27
|
+
const state = decodeURIComponent(context.query.state || '')
|
|
28
|
+
const code = decodeURIComponent(context.query.code || '')
|
|
29
|
+
|
|
30
|
+
const stateInCookie = context.getCookie('state')
|
|
31
|
+
|
|
32
|
+
if (state !== stateInCookie) throw new Error('Authentication failed')
|
|
33
|
+
|
|
34
|
+
const codeVerifier = context.getCookie('code_verifier')
|
|
35
|
+
const provider = context.getCookie('provider')
|
|
36
|
+
|
|
37
|
+
const { signInWith } = await getGraphqlClient(
|
|
38
|
+
wabeContext.wabe.config.port,
|
|
39
|
+
).request<any>(
|
|
40
|
+
gql`
|
|
41
|
+
mutation signInWith(
|
|
42
|
+
$authorizationCode: String!
|
|
43
|
+
$codeVerifier: String!
|
|
44
|
+
) {
|
|
45
|
+
signInWith(
|
|
46
|
+
input: {
|
|
47
|
+
authentication: {
|
|
48
|
+
${provider}: {
|
|
49
|
+
authorizationCode: $authorizationCode
|
|
50
|
+
codeVerifier: $codeVerifier
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
){
|
|
55
|
+
accessToken
|
|
56
|
+
refreshToken
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
`,
|
|
60
|
+
{
|
|
61
|
+
authorizationCode: code,
|
|
62
|
+
codeVerifier,
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const { accessToken, refreshToken } = signInWith
|
|
67
|
+
|
|
68
|
+
const isCookieSession =
|
|
69
|
+
!!wabeContext.wabe.config.authentication?.session?.cookieSession
|
|
70
|
+
|
|
71
|
+
context.res.setCookie('accessToken', accessToken, {
|
|
72
|
+
// If cookie session we put httpOnly to true, otherwise the front will need to get it
|
|
73
|
+
// So we keep it to false
|
|
74
|
+
httpOnly: isCookieSession,
|
|
75
|
+
path: '/',
|
|
76
|
+
maxAge:
|
|
77
|
+
(wabeContext.wabe.config.authentication?.session
|
|
78
|
+
?.accessTokenExpiresInMs || 60 * 15 * 1000) / 1000, // 15 minutes in seconds
|
|
79
|
+
sameSite: 'None',
|
|
80
|
+
secure: true,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
context.res.setCookie('refreshToken', refreshToken, {
|
|
84
|
+
// If cookie session we put httpOnly to true, otherwise the front will need to get it
|
|
85
|
+
// So we keep it to false
|
|
86
|
+
httpOnly: isCookieSession,
|
|
87
|
+
path: '/',
|
|
88
|
+
maxAge:
|
|
89
|
+
(wabeContext.wabe.config.authentication?.session
|
|
90
|
+
?.accessTokenExpiresInMs || 60 * 15 * 1000) / 1000, // 15 minutes in seconds
|
|
91
|
+
sameSite: 'None',
|
|
92
|
+
secure: true,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
context.redirect(
|
|
96
|
+
wabeContext.wabe.config.authentication?.successRedirectPath || '/',
|
|
97
|
+
)
|
|
98
|
+
} catch {
|
|
99
|
+
context.redirect(
|
|
100
|
+
wabeContext.wabe.config.authentication?.failureRedirectPath || '/',
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const authHandler = (
|
|
106
|
+
context: Context,
|
|
107
|
+
wabeContext: WabeContext<any>,
|
|
108
|
+
provider: ProviderEnum,
|
|
109
|
+
) => {
|
|
110
|
+
if (!wabeContext.wabe.config) throw new Error('Wabe config not found')
|
|
111
|
+
|
|
112
|
+
context.res.setCookie('provider', provider, {
|
|
113
|
+
httpOnly: true,
|
|
114
|
+
path: '/',
|
|
115
|
+
maxAge: 60 * 5, // 5 minutes
|
|
116
|
+
secure: true,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
switch (provider) {
|
|
120
|
+
case ProviderEnum.google: {
|
|
121
|
+
const googleOauth = new Google(wabeContext.wabe.config)
|
|
122
|
+
|
|
123
|
+
const state = generateRandomValues()
|
|
124
|
+
const codeVerifier = generateRandomValues()
|
|
125
|
+
|
|
126
|
+
context.res.setCookie('code_verifier', codeVerifier, {
|
|
127
|
+
httpOnly: true,
|
|
128
|
+
path: '/',
|
|
129
|
+
maxAge: 60 * 5, // 5 minutes
|
|
130
|
+
secure: true,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
context.res.setCookie('state', state, {
|
|
134
|
+
httpOnly: true,
|
|
135
|
+
path: '/',
|
|
136
|
+
maxAge: 60 * 5, // 5 minutes
|
|
137
|
+
secure: true,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const authorizationURL = googleOauth.createAuthorizationURL(
|
|
141
|
+
state,
|
|
142
|
+
codeVerifier,
|
|
143
|
+
{
|
|
144
|
+
scopes: ['email'],
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
context.redirect(authorizationURL.toString())
|
|
149
|
+
|
|
150
|
+
break
|
|
151
|
+
}
|
|
152
|
+
case ProviderEnum.github: {
|
|
153
|
+
const githubOauth = new GitHub(wabeContext.wabe.config)
|
|
154
|
+
|
|
155
|
+
const state = generateRandomValues()
|
|
156
|
+
const codeVerifier = generateRandomValues()
|
|
157
|
+
|
|
158
|
+
context.res.setCookie('code_verifier', codeVerifier, {
|
|
159
|
+
httpOnly: true,
|
|
160
|
+
path: '/',
|
|
161
|
+
maxAge: 60 * 5, // 5 minutes
|
|
162
|
+
secure: true,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
context.res.setCookie('state', state, {
|
|
166
|
+
httpOnly: true,
|
|
167
|
+
path: '/',
|
|
168
|
+
maxAge: 60 * 5, // 5 minutes
|
|
169
|
+
secure: true,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const authorizationURL = githubOauth.createAuthorizationURL(
|
|
173
|
+
state,
|
|
174
|
+
codeVerifier,
|
|
175
|
+
{
|
|
176
|
+
scopes: ['email'],
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
context.redirect(authorizationURL.toString())
|
|
181
|
+
|
|
182
|
+
break
|
|
183
|
+
}
|
|
184
|
+
default:
|
|
185
|
+
break
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type WobeHandler, uploadDirectory } from 'wobe'
|
|
2
|
+
import type { ProviderEnum } from '../../authentication/interface'
|
|
3
|
+
import { authHandler, oauthHandlerCallback } from './authHandler'
|
|
4
|
+
import type { WobeCustomContext } from '..'
|
|
5
|
+
|
|
6
|
+
export interface WabeRoute {
|
|
7
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
|
8
|
+
path: string
|
|
9
|
+
handler: WobeHandler<WobeCustomContext<any>>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const defaultRoutes = (devDirectory: string): WabeRoute[] => {
|
|
13
|
+
const routes: WabeRoute[] = [
|
|
14
|
+
{
|
|
15
|
+
method: 'GET',
|
|
16
|
+
path: '/auth/oauth',
|
|
17
|
+
handler: (context) => {
|
|
18
|
+
const provider = context.query.provider
|
|
19
|
+
|
|
20
|
+
if (!provider)
|
|
21
|
+
throw new Error('Authentication failed, provider not found')
|
|
22
|
+
|
|
23
|
+
// TODO: Maybe check if the value is in the enum
|
|
24
|
+
return authHandler(context, context.wabe, provider as ProviderEnum)
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
method: 'GET',
|
|
29
|
+
path: '/auth/oauth/callback',
|
|
30
|
+
handler: (context) => oauthHandlerCallback(context, context.wabe),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
method: 'GET',
|
|
34
|
+
path: '/bucket/:filename',
|
|
35
|
+
handler: uploadDirectory({ directory: devDirectory }),
|
|
36
|
+
},
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
return routes
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { encryptDeterministicToken, decryptDeterministicToken } from './crypto'
|
|
3
|
+
|
|
4
|
+
const key = Buffer.alloc(32, 1) // deterministic test key
|
|
5
|
+
|
|
6
|
+
describe('crypto deterministic token helpers', () => {
|
|
7
|
+
it('should encrypt and decrypt deterministically with same key', () => {
|
|
8
|
+
const token = 'my-token'
|
|
9
|
+
const encrypted = encryptDeterministicToken(token, key)
|
|
10
|
+
const decrypted = decryptDeterministicToken(encrypted, key)
|
|
11
|
+
|
|
12
|
+
expect(decrypted).toBe(token)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should produce the same ciphertext for the same token/key', () => {
|
|
16
|
+
const token = 'stable'
|
|
17
|
+
const enc1 = encryptDeterministicToken(token, key)
|
|
18
|
+
const enc2 = encryptDeterministicToken(token, key)
|
|
19
|
+
|
|
20
|
+
expect(enc1).toBe(enc2)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should produce different ciphertexts for different tokens', () => {
|
|
24
|
+
const enc1 = encryptDeterministicToken('a', key)
|
|
25
|
+
const enc2 = encryptDeterministicToken('b', key)
|
|
26
|
+
|
|
27
|
+
expect(enc1).not.toBe(enc2)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should return null when decrypting with the wrong key', () => {
|
|
31
|
+
const token = 'secret'
|
|
32
|
+
const encrypted = encryptDeterministicToken(token, key)
|
|
33
|
+
const wrongKey = Buffer.alloc(32, 2)
|
|
34
|
+
|
|
35
|
+
expect(decryptDeterministicToken(encrypted, wrongKey)).toBeNull()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should return null on malformed ciphertext', () => {
|
|
39
|
+
expect(decryptDeterministicToken('bad:format', key)).toBeNull()
|
|
40
|
+
})
|
|
41
|
+
})
|