@nixxie-cms/auth 1.0.1 → 1.1.0
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 +6 -6
- package/components/Navigation/package.json +4 -4
- package/dist/declarations/src/index.d.ts +2 -2
- package/dist/declarations/src/index.d.ts.map +1 -1
- package/dist/declarations/src/types.d.ts +9 -9
- package/dist/declarations/src/types.d.ts.map +1 -1
- package/dist/nixxie-cms-auth.cjs.js +22 -20
- package/dist/nixxie-cms-auth.esm.js +22 -20
- package/package.json +2 -2
- package/pages/InitPage/dist/nixxie-cms-auth-pages-InitPage.cjs.js +1 -1
- package/pages/InitPage/dist/nixxie-cms-auth-pages-InitPage.esm.js +1 -1
- package/pages/InitPage/package.json +4 -4
- package/pages/SigninPage/dist/nixxie-cms-auth-pages-SigninPage.cjs.js +1 -1
- package/pages/SigninPage/dist/nixxie-cms-auth-pages-SigninPage.esm.js +1 -1
- package/pages/SigninPage/package.json +4 -4
- package/src/gql/getInitFirstItemSchema.ts +87 -87
- package/src/index.ts +291 -291
- package/src/schema.ts +85 -84
- package/src/templates/config.ts +9 -9
- package/src/templates/init.ts +22 -22
- package/src/templates/signin.ts +20 -20
- package/src/types.ts +57 -57
- /package/dist/{useFromRedirect-2de239a9.cjs.js → useFromRedirect-80f27dbb.cjs.js} +0 -0
- /package/dist/{useFromRedirect-b3deee00.esm.js → useFromRedirect-e80750d8.esm.js} +0 -0
package/src/index.ts
CHANGED
|
@@ -1,291 +1,291 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AdminFileToWrite,
|
|
3
|
-
|
|
4
|
-
NixxieContext,
|
|
5
|
-
SessionStrategy,
|
|
6
|
-
BaseNixxieTypeInfo,
|
|
7
|
-
NixxieConfig,
|
|
8
|
-
} from '@nixxie-cms/core/types'
|
|
9
|
-
import type { AuthConfig, AuthGqlNames } from './types'
|
|
10
|
-
|
|
11
|
-
import { getSchemaExtension } from './schema'
|
|
12
|
-
import configTemplate from './templates/config'
|
|
13
|
-
import signinTemplate from './templates/signin'
|
|
14
|
-
import initTemplate from './templates/init'
|
|
15
|
-
|
|
16
|
-
export type AuthSession = {
|
|
17
|
-
itemId: string | number // TODO: use
|
|
18
|
-
data: unknown // TODO: use
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function getAuthGqlNames(singular: string): AuthGqlNames {
|
|
22
|
-
const lowerSingularName = singular.charAt(0).toLowerCase() + singular.slice(1)
|
|
23
|
-
return {
|
|
24
|
-
itemQueryName: lowerSingularName,
|
|
25
|
-
whereUniqueInputName: `${singular}WhereUniqueInput`,
|
|
26
|
-
|
|
27
|
-
authenticateItemWithPassword: `authenticate${singular}WithPassword`,
|
|
28
|
-
ItemAuthenticationWithPasswordResult: `${singular}AuthenticationWithPasswordResult`,
|
|
29
|
-
ItemAuthenticationWithPasswordSuccess: `${singular}AuthenticationWithPasswordSuccess`,
|
|
30
|
-
ItemAuthenticationWithPasswordFailure: `${singular}AuthenticationWithPasswordFailure`,
|
|
31
|
-
|
|
32
|
-
CreateInitialInput: `CreateInitial${singular}Input`,
|
|
33
|
-
createInitialItem: `createInitial${singular}`,
|
|
34
|
-
} as const
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// TODO: use TypeInfo and listKey for types
|
|
38
|
-
/**
|
|
39
|
-
* createAuth function
|
|
40
|
-
*
|
|
41
|
-
* Generates config for Nixxie to implement standard auth features.
|
|
42
|
-
*/
|
|
43
|
-
export function createAuth<
|
|
44
|
-
listKey,
|
|
45
|
-
secretField,
|
|
46
|
-
initFirstItem,
|
|
47
|
-
identityField,
|
|
48
|
-
sessionData = 'id',
|
|
49
|
-
}: AuthConfig<
|
|
50
|
-
/**
|
|
51
|
-
* getAdditionalFiles
|
|
52
|
-
*
|
|
53
|
-
* This function adds files to be generated into the Admin UI build. Must be added to the
|
|
54
|
-
* ui.getAdditionalFiles config.
|
|
55
|
-
*
|
|
56
|
-
* The signin page is always included, and the init page is included when initFirstItem is set
|
|
57
|
-
*/
|
|
58
|
-
const authGetAdditionalFiles = (config: NixxieConfig) => {
|
|
59
|
-
// TODO: FIXME: this is a duplication of initialise-lists:747
|
|
60
|
-
const listConfig = config.lists[listKey]
|
|
61
|
-
const labelField =
|
|
62
|
-
listConfig.ui?.labelField ??
|
|
63
|
-
(listConfig.fields.label
|
|
64
|
-
? 'label'
|
|
65
|
-
: listConfig.fields.name
|
|
66
|
-
? 'name'
|
|
67
|
-
: listConfig.fields.title
|
|
68
|
-
? 'title'
|
|
69
|
-
: 'id')
|
|
70
|
-
|
|
71
|
-
const authGqlNames = getAuthGqlNames(listConfig.graphql?.singular ?? listKey)
|
|
72
|
-
const filesToWrite: AdminFileToWrite[] = [
|
|
73
|
-
{
|
|
74
|
-
mode: 'write',
|
|
75
|
-
src: signinTemplate({ authGqlNames, identityField, secretField }),
|
|
76
|
-
outputPath: 'pages/signin.js',
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
mode: 'write',
|
|
80
|
-
src: configTemplate({ labelField }),
|
|
81
|
-
outputPath: 'config.ts',
|
|
82
|
-
},
|
|
83
|
-
]
|
|
84
|
-
if (initFirstItem) {
|
|
85
|
-
filesToWrite.push({
|
|
86
|
-
mode: 'write',
|
|
87
|
-
src: initTemplate({ authGqlNames, listKey, initFirstItem }),
|
|
88
|
-
outputPath: 'pages/init.js',
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
return filesToWrite
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function throwIfInvalidConfig<TypeInfo extends BaseNixxieTypeInfo>(
|
|
95
|
-
config: NixxieConfig<TypeInfo>
|
|
96
|
-
) {
|
|
97
|
-
if (!(listKey in config.lists)) {
|
|
98
|
-
throw new Error(`withAuth cannot find the list "${listKey}"`)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// TODO: verify that the identity field is unique
|
|
102
|
-
// TODO: verify that the field is required
|
|
103
|
-
const list = config.lists[listKey]
|
|
104
|
-
if (!(identityField in list.fields)) {
|
|
105
|
-
throw new Error(`withAuth cannot find the identity field "${listKey}.${identityField}"`)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (!(secretField in list.fields)) {
|
|
109
|
-
throw new Error(`withAuth cannot find the secret field "${listKey}.${secretField}"`)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
for (const fieldKey of initFirstItem?.fields || []) {
|
|
113
|
-
if (fieldKey in list.fields) continue
|
|
114
|
-
|
|
115
|
-
throw new Error(`initFirstItem.fields has unknown field "${listKey}.${fieldKey}"`)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// this strategy wraps the existing session strategy,
|
|
120
|
-
// and injects the requested session.data before returning
|
|
121
|
-
function authSessionStrategy<Session extends AuthSession>(
|
|
122
|
-
_sessionStrategy: SessionStrategy<Session>
|
|
123
|
-
): SessionStrategy<Session> {
|
|
124
|
-
const { get, ...sessionStrategy } = _sessionStrategy
|
|
125
|
-
return {
|
|
126
|
-
...sessionStrategy,
|
|
127
|
-
get: async ({ context }) => {
|
|
128
|
-
const session = await get({ context })
|
|
129
|
-
const sudoContext = context.sudo()
|
|
130
|
-
if (!session?.itemId) return
|
|
131
|
-
|
|
132
|
-
// TODO: replace with SessionSecret: HMAC({ listKey, identityField, secretField }, SessionSecretVar)
|
|
133
|
-
// if (session.listKey !== listKey) return null
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
const data = await sudoContext.query[listKey].findOne({
|
|
137
|
-
where: { id: session.itemId },
|
|
138
|
-
query: sessionData,
|
|
139
|
-
})
|
|
140
|
-
if (!data) return
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
...session,
|
|
144
|
-
itemId: session.itemId,
|
|
145
|
-
data,
|
|
146
|
-
}
|
|
147
|
-
} catch (e) {
|
|
148
|
-
console.error(e)
|
|
149
|
-
// WARNING: this is probably an invalid configuration
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function hasInitFirstItemConditions<TypeInfo extends BaseNixxieTypeInfo>(
|
|
157
|
-
context: NixxieContext<TypeInfo>
|
|
158
|
-
) {
|
|
159
|
-
// do nothing if they aren't using this feature
|
|
160
|
-
if (!initFirstItem) return false
|
|
161
|
-
|
|
162
|
-
// if they have a session, there is no initialisation necessary
|
|
163
|
-
if (context.session) return false
|
|
164
|
-
|
|
165
|
-
const count = await context.sudo().db[listKey].count({})
|
|
166
|
-
return count === 0
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async function authMiddleware<TypeInfo extends BaseNixxieTypeInfo>({
|
|
170
|
-
context,
|
|
171
|
-
wasAccessAllowed,
|
|
172
|
-
basePath,
|
|
173
|
-
}: {
|
|
174
|
-
context: NixxieContext<TypeInfo>
|
|
175
|
-
wasAccessAllowed: boolean
|
|
176
|
-
basePath: string
|
|
177
|
-
}): Promise<{ kind: 'redirect'; to: string } | void> {
|
|
178
|
-
const { req } = context
|
|
179
|
-
const { pathname } = new URL(req!.url!, 'http://_')
|
|
180
|
-
|
|
181
|
-
// redirect to init if initFirstItem conditions are met
|
|
182
|
-
if (pathname !== `${basePath}/init` && (await hasInitFirstItemConditions(context))) {
|
|
183
|
-
return { kind: 'redirect', to: `${basePath}/init` }
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// redirect to / if attempting to /init and initFirstItem conditions are not met
|
|
187
|
-
if (pathname === `${basePath}/init` && !(await hasInitFirstItemConditions(context))) {
|
|
188
|
-
return { kind: 'redirect', to: basePath }
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// don't redirect if we have access
|
|
192
|
-
if (wasAccessAllowed) return
|
|
193
|
-
|
|
194
|
-
// otherwise, redirect to signin
|
|
195
|
-
return { kind: 'redirect', to: `${basePath}/signin` }
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function defaultIsAccessAllowed({ session }: NixxieContext) {
|
|
199
|
-
return session !== undefined
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function defaultExtendGraphqlSchema<T>(schema: T) {
|
|
203
|
-
return schema
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* withAuth
|
|
208
|
-
*
|
|
209
|
-
* Automatically extends your configuration with a prescriptive implementation.
|
|
210
|
-
*/
|
|
211
|
-
function withAuth<TypeInfo extends BaseNixxieTypeInfo>(
|
|
212
|
-
config: NixxieConfig<TypeInfo>
|
|
213
|
-
): NixxieConfig<TypeInfo> {
|
|
214
|
-
throwIfInvalidConfig(config)
|
|
215
|
-
let { ui } = config
|
|
216
|
-
if (!ui?.isDisabled) {
|
|
217
|
-
const {
|
|
218
|
-
getAdditionalFiles = () => [],
|
|
219
|
-
isAccessAllowed = defaultIsAccessAllowed,
|
|
220
|
-
pageMiddleware,
|
|
221
|
-
publicPages = [],
|
|
222
|
-
} = ui || {}
|
|
223
|
-
const authPublicPages = [`${ui?.basePath ?? ''}/signin`]
|
|
224
|
-
ui = {
|
|
225
|
-
...ui,
|
|
226
|
-
publicPages: [...publicPages, ...authPublicPages],
|
|
227
|
-
getAdditionalFiles: async () => [
|
|
228
|
-
...(await getAdditionalFiles()),
|
|
229
|
-
...authGetAdditionalFiles(config),
|
|
230
|
-
],
|
|
231
|
-
|
|
232
|
-
isAccessAllowed: async (context: NixxieContext) => {
|
|
233
|
-
if (await hasInitFirstItemConditions(context)) return true
|
|
234
|
-
return isAccessAllowed(context)
|
|
235
|
-
},
|
|
236
|
-
|
|
237
|
-
pageMiddleware: async args => {
|
|
238
|
-
const shouldRedirect = await authMiddleware(args)
|
|
239
|
-
if (shouldRedirect) return shouldRedirect
|
|
240
|
-
return pageMiddleware?.(args)
|
|
241
|
-
},
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (!config.session) throw new TypeError('Missing .session configuration')
|
|
246
|
-
|
|
247
|
-
const { graphql } = config
|
|
248
|
-
const { extendGraphqlSchema = defaultExtendGraphqlSchema } = graphql ?? {}
|
|
249
|
-
const listConfig = config.lists[listKey]
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* extendGraphqlSchema
|
|
253
|
-
*
|
|
254
|
-
* Must be added to the extendGraphqlSchema config. Can be composed.
|
|
255
|
-
*/
|
|
256
|
-
const authGqlNames = getAuthGqlNames(listConfig.graphql?.singular ?? listKey)
|
|
257
|
-
const authExtendGraphqlSchema = getSchemaExtension({
|
|
258
|
-
authGqlNames,
|
|
259
|
-
listKey,
|
|
260
|
-
identityField,
|
|
261
|
-
secretField,
|
|
262
|
-
initFirstItem,
|
|
263
|
-
sessionData,
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
return {
|
|
267
|
-
...config,
|
|
268
|
-
graphql: {
|
|
269
|
-
...config.graphql,
|
|
270
|
-
extendGraphqlSchema: schema => {
|
|
271
|
-
return extendGraphqlSchema(authExtendGraphqlSchema(schema))
|
|
272
|
-
},
|
|
273
|
-
},
|
|
274
|
-
ui,
|
|
275
|
-
session: authSessionStrategy(config.session),
|
|
276
|
-
lists: {
|
|
277
|
-
...config.lists,
|
|
278
|
-
[listKey]: {
|
|
279
|
-
...listConfig,
|
|
280
|
-
fields: {
|
|
281
|
-
...listConfig.fields,
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return {
|
|
289
|
-
withAuth,
|
|
290
|
-
}
|
|
291
|
-
}
|
|
1
|
+
import type {
|
|
2
|
+
AdminFileToWrite,
|
|
3
|
+
BaseCollectionTypeInfo,
|
|
4
|
+
NixxieContext,
|
|
5
|
+
SessionStrategy,
|
|
6
|
+
BaseNixxieTypeInfo,
|
|
7
|
+
NixxieConfig,
|
|
8
|
+
} from '@nixxie-cms/core/types'
|
|
9
|
+
import type { AuthConfig, AuthGqlNames } from './types'
|
|
10
|
+
|
|
11
|
+
import { getSchemaExtension } from './schema'
|
|
12
|
+
import configTemplate from './templates/config'
|
|
13
|
+
import signinTemplate from './templates/signin'
|
|
14
|
+
import initTemplate from './templates/init'
|
|
15
|
+
|
|
16
|
+
export type AuthSession = {
|
|
17
|
+
itemId: string | number // TODO: use CollectionTypeInfo
|
|
18
|
+
data: unknown // TODO: use CollectionTypeInfo
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getAuthGqlNames(singular: string): AuthGqlNames {
|
|
22
|
+
const lowerSingularName = singular.charAt(0).toLowerCase() + singular.slice(1)
|
|
23
|
+
return {
|
|
24
|
+
itemQueryName: lowerSingularName,
|
|
25
|
+
whereUniqueInputName: `${singular}WhereUniqueInput`,
|
|
26
|
+
|
|
27
|
+
authenticateItemWithPassword: `authenticate${singular}WithPassword`,
|
|
28
|
+
ItemAuthenticationWithPasswordResult: `${singular}AuthenticationWithPasswordResult`,
|
|
29
|
+
ItemAuthenticationWithPasswordSuccess: `${singular}AuthenticationWithPasswordSuccess`,
|
|
30
|
+
ItemAuthenticationWithPasswordFailure: `${singular}AuthenticationWithPasswordFailure`,
|
|
31
|
+
|
|
32
|
+
CreateInitialInput: `CreateInitial${singular}Input`,
|
|
33
|
+
createInitialItem: `createInitial${singular}`,
|
|
34
|
+
} as const
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// TODO: use TypeInfo and listKey for types
|
|
38
|
+
/**
|
|
39
|
+
* createAuth function
|
|
40
|
+
*
|
|
41
|
+
* Generates config for Nixxie to implement standard auth features.
|
|
42
|
+
*/
|
|
43
|
+
export function createAuth<CollectionTypeInfo extends BaseCollectionTypeInfo>({
|
|
44
|
+
listKey,
|
|
45
|
+
secretField,
|
|
46
|
+
initFirstItem,
|
|
47
|
+
identityField,
|
|
48
|
+
sessionData = 'id',
|
|
49
|
+
}: AuthConfig<CollectionTypeInfo>) {
|
|
50
|
+
/**
|
|
51
|
+
* getAdditionalFiles
|
|
52
|
+
*
|
|
53
|
+
* This function adds files to be generated into the Admin UI build. Must be added to the
|
|
54
|
+
* ui.getAdditionalFiles config.
|
|
55
|
+
*
|
|
56
|
+
* The signin page is always included, and the init page is included when initFirstItem is set
|
|
57
|
+
*/
|
|
58
|
+
const authGetAdditionalFiles = (config: NixxieConfig) => {
|
|
59
|
+
// TODO: FIXME: this is a duplication of initialise-lists:747
|
|
60
|
+
const listConfig = config.lists[listKey]
|
|
61
|
+
const labelField =
|
|
62
|
+
listConfig.ui?.labelField ??
|
|
63
|
+
(listConfig.fields.label
|
|
64
|
+
? 'label'
|
|
65
|
+
: listConfig.fields.name
|
|
66
|
+
? 'name'
|
|
67
|
+
: listConfig.fields.title
|
|
68
|
+
? 'title'
|
|
69
|
+
: 'id')
|
|
70
|
+
|
|
71
|
+
const authGqlNames = getAuthGqlNames(listConfig.graphql?.singular ?? listKey)
|
|
72
|
+
const filesToWrite: AdminFileToWrite[] = [
|
|
73
|
+
{
|
|
74
|
+
mode: 'write',
|
|
75
|
+
src: signinTemplate({ authGqlNames, identityField, secretField }),
|
|
76
|
+
outputPath: 'pages/signin.js',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
mode: 'write',
|
|
80
|
+
src: configTemplate({ labelField }),
|
|
81
|
+
outputPath: 'config.ts',
|
|
82
|
+
},
|
|
83
|
+
]
|
|
84
|
+
if (initFirstItem) {
|
|
85
|
+
filesToWrite.push({
|
|
86
|
+
mode: 'write',
|
|
87
|
+
src: initTemplate({ authGqlNames, listKey, initFirstItem }),
|
|
88
|
+
outputPath: 'pages/init.js',
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
return filesToWrite
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function throwIfInvalidConfig<TypeInfo extends BaseNixxieTypeInfo>(
|
|
95
|
+
config: NixxieConfig<TypeInfo>
|
|
96
|
+
) {
|
|
97
|
+
if (!(listKey in config.lists)) {
|
|
98
|
+
throw new Error(`withAuth cannot find the list "${listKey}"`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// TODO: verify that the identity field is unique
|
|
102
|
+
// TODO: verify that the field is required
|
|
103
|
+
const list = config.lists[listKey]
|
|
104
|
+
if (!(identityField in list.fields)) {
|
|
105
|
+
throw new Error(`withAuth cannot find the identity field "${listKey}.${identityField}"`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!(secretField in list.fields)) {
|
|
109
|
+
throw new Error(`withAuth cannot find the secret field "${listKey}.${secretField}"`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const fieldKey of initFirstItem?.fields || []) {
|
|
113
|
+
if (fieldKey in list.fields) continue
|
|
114
|
+
|
|
115
|
+
throw new Error(`initFirstItem.fields has unknown field "${listKey}.${fieldKey}"`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// this strategy wraps the existing session strategy,
|
|
120
|
+
// and injects the requested session.data before returning
|
|
121
|
+
function authSessionStrategy<Session extends AuthSession>(
|
|
122
|
+
_sessionStrategy: SessionStrategy<Session>
|
|
123
|
+
): SessionStrategy<Session> {
|
|
124
|
+
const { get, ...sessionStrategy } = _sessionStrategy
|
|
125
|
+
return {
|
|
126
|
+
...sessionStrategy,
|
|
127
|
+
get: async ({ context }) => {
|
|
128
|
+
const session = await get({ context })
|
|
129
|
+
const sudoContext = context.sudo()
|
|
130
|
+
if (!session?.itemId) return
|
|
131
|
+
|
|
132
|
+
// TODO: replace with SessionSecret: HMAC({ listKey, identityField, secretField }, SessionSecretVar)
|
|
133
|
+
// if (session.listKey !== listKey) return null
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const data = await sudoContext.query[listKey].findOne({
|
|
137
|
+
where: { id: session.itemId },
|
|
138
|
+
query: sessionData,
|
|
139
|
+
})
|
|
140
|
+
if (!data) return
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
...session,
|
|
144
|
+
itemId: session.itemId,
|
|
145
|
+
data,
|
|
146
|
+
}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
console.error(e)
|
|
149
|
+
// WARNING: this is probably an invalid configuration
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function hasInitFirstItemConditions<TypeInfo extends BaseNixxieTypeInfo>(
|
|
157
|
+
context: NixxieContext<TypeInfo>
|
|
158
|
+
) {
|
|
159
|
+
// do nothing if they aren't using this feature
|
|
160
|
+
if (!initFirstItem) return false
|
|
161
|
+
|
|
162
|
+
// if they have a session, there is no initialisation necessary
|
|
163
|
+
if (context.session) return false
|
|
164
|
+
|
|
165
|
+
const count = await context.sudo().db[listKey].count({})
|
|
166
|
+
return count === 0
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function authMiddleware<TypeInfo extends BaseNixxieTypeInfo>({
|
|
170
|
+
context,
|
|
171
|
+
wasAccessAllowed,
|
|
172
|
+
basePath,
|
|
173
|
+
}: {
|
|
174
|
+
context: NixxieContext<TypeInfo>
|
|
175
|
+
wasAccessAllowed: boolean
|
|
176
|
+
basePath: string
|
|
177
|
+
}): Promise<{ kind: 'redirect'; to: string } | void> {
|
|
178
|
+
const { req } = context
|
|
179
|
+
const { pathname } = new URL(req!.url!, 'http://_')
|
|
180
|
+
|
|
181
|
+
// redirect to init if initFirstItem conditions are met
|
|
182
|
+
if (pathname !== `${basePath}/init` && (await hasInitFirstItemConditions(context))) {
|
|
183
|
+
return { kind: 'redirect', to: `${basePath}/init` }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// redirect to / if attempting to /init and initFirstItem conditions are not met
|
|
187
|
+
if (pathname === `${basePath}/init` && !(await hasInitFirstItemConditions(context))) {
|
|
188
|
+
return { kind: 'redirect', to: basePath }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// don't redirect if we have access
|
|
192
|
+
if (wasAccessAllowed) return
|
|
193
|
+
|
|
194
|
+
// otherwise, redirect to signin
|
|
195
|
+
return { kind: 'redirect', to: `${basePath}/signin` }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function defaultIsAccessAllowed({ session }: NixxieContext) {
|
|
199
|
+
return session !== undefined
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function defaultExtendGraphqlSchema<T>(schema: T) {
|
|
203
|
+
return schema
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* withAuth
|
|
208
|
+
*
|
|
209
|
+
* Automatically extends your configuration with a prescriptive implementation.
|
|
210
|
+
*/
|
|
211
|
+
function withAuth<TypeInfo extends BaseNixxieTypeInfo>(
|
|
212
|
+
config: NixxieConfig<TypeInfo>
|
|
213
|
+
): NixxieConfig<TypeInfo> {
|
|
214
|
+
throwIfInvalidConfig(config)
|
|
215
|
+
let { ui } = config
|
|
216
|
+
if (!ui?.isDisabled) {
|
|
217
|
+
const {
|
|
218
|
+
getAdditionalFiles = () => [],
|
|
219
|
+
isAccessAllowed = defaultIsAccessAllowed,
|
|
220
|
+
pageMiddleware,
|
|
221
|
+
publicPages = [],
|
|
222
|
+
} = ui || {}
|
|
223
|
+
const authPublicPages = [`${ui?.basePath ?? ''}/signin`]
|
|
224
|
+
ui = {
|
|
225
|
+
...ui,
|
|
226
|
+
publicPages: [...publicPages, ...authPublicPages],
|
|
227
|
+
getAdditionalFiles: async () => [
|
|
228
|
+
...(await getAdditionalFiles()),
|
|
229
|
+
...authGetAdditionalFiles(config),
|
|
230
|
+
],
|
|
231
|
+
|
|
232
|
+
isAccessAllowed: async (context: NixxieContext) => {
|
|
233
|
+
if (await hasInitFirstItemConditions(context)) return true
|
|
234
|
+
return isAccessAllowed(context)
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
pageMiddleware: async args => {
|
|
238
|
+
const shouldRedirect = await authMiddleware(args)
|
|
239
|
+
if (shouldRedirect) return shouldRedirect
|
|
240
|
+
return pageMiddleware?.(args)
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!config.session) throw new TypeError('Missing .session configuration')
|
|
246
|
+
|
|
247
|
+
const { graphql } = config
|
|
248
|
+
const { extendGraphqlSchema = defaultExtendGraphqlSchema } = graphql ?? {}
|
|
249
|
+
const listConfig = config.lists[listKey]
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* extendGraphqlSchema
|
|
253
|
+
*
|
|
254
|
+
* Must be added to the extendGraphqlSchema config. Can be composed.
|
|
255
|
+
*/
|
|
256
|
+
const authGqlNames = getAuthGqlNames(listConfig.graphql?.singular ?? listKey)
|
|
257
|
+
const authExtendGraphqlSchema = getSchemaExtension({
|
|
258
|
+
authGqlNames,
|
|
259
|
+
listKey,
|
|
260
|
+
identityField,
|
|
261
|
+
secretField,
|
|
262
|
+
initFirstItem,
|
|
263
|
+
sessionData,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
...config,
|
|
268
|
+
graphql: {
|
|
269
|
+
...config.graphql,
|
|
270
|
+
extendGraphqlSchema: schema => {
|
|
271
|
+
return extendGraphqlSchema(authExtendGraphqlSchema(schema))
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
ui,
|
|
275
|
+
session: authSessionStrategy(config.session),
|
|
276
|
+
lists: {
|
|
277
|
+
...config.lists,
|
|
278
|
+
[listKey]: {
|
|
279
|
+
...listConfig,
|
|
280
|
+
fields: {
|
|
281
|
+
...listConfig.fields,
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
withAuth,
|
|
290
|
+
}
|
|
291
|
+
}
|