@nixxie-cms/auth 1.0.1

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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -0
  3. package/components/Navigation/dist/nixxie-cms-auth-components-Navigation.cjs.d.ts +3 -0
  4. package/components/Navigation/dist/nixxie-cms-auth-components-Navigation.cjs.js +147 -0
  5. package/components/Navigation/dist/nixxie-cms-auth-components-Navigation.esm.js +143 -0
  6. package/components/Navigation/package.json +4 -0
  7. package/dist/declarations/src/components/Navigation.d.ts +6 -0
  8. package/dist/declarations/src/components/Navigation.d.ts.map +1 -0
  9. package/dist/declarations/src/index.d.ts +15 -0
  10. package/dist/declarations/src/index.d.ts.map +1 -0
  11. package/dist/declarations/src/pages/InitPage.d.ts +9 -0
  12. package/dist/declarations/src/pages/InitPage.d.ts.map +1 -0
  13. package/dist/declarations/src/pages/SigninPage.d.ts +9 -0
  14. package/dist/declarations/src/pages/SigninPage.d.ts.map +1 -0
  15. package/dist/declarations/src/types.d.ts +49 -0
  16. package/dist/declarations/src/types.d.ts.map +1 -0
  17. package/dist/nixxie-cms-auth.cjs.d.ts +2 -0
  18. package/dist/nixxie-cms-auth.cjs.js +552 -0
  19. package/dist/nixxie-cms-auth.esm.js +548 -0
  20. package/dist/useFromRedirect-2de239a9.cjs.js +26 -0
  21. package/dist/useFromRedirect-b3deee00.esm.js +24 -0
  22. package/package.json +56 -0
  23. package/pages/InitPage/dist/nixxie-cms-auth-pages-InitPage.cjs.d.ts +3 -0
  24. package/pages/InitPage/dist/nixxie-cms-auth-pages-InitPage.cjs.js +274 -0
  25. package/pages/InitPage/dist/nixxie-cms-auth-pages-InitPage.esm.js +266 -0
  26. package/pages/InitPage/package.json +4 -0
  27. package/pages/SigninPage/dist/nixxie-cms-auth-pages-SigninPage.cjs.d.ts +3 -0
  28. package/pages/SigninPage/dist/nixxie-cms-auth-pages-SigninPage.cjs.js +319 -0
  29. package/pages/SigninPage/dist/nixxie-cms-auth-pages-SigninPage.esm.js +311 -0
  30. package/pages/SigninPage/package.json +4 -0
  31. package/src/components/Navigation.tsx +182 -0
  32. package/src/gql/getBaseAuthSchema.ts +129 -0
  33. package/src/gql/getInitFirstItemSchema.ts +87 -0
  34. package/src/index.ts +291 -0
  35. package/src/lib/useFromRedirect.ts +23 -0
  36. package/src/pages/InitPage.tsx +292 -0
  37. package/src/pages/SigninPage.tsx +331 -0
  38. package/src/schema.ts +84 -0
  39. package/src/templates/config.ts +9 -0
  40. package/src/templates/init.ts +22 -0
  41. package/src/templates/signin.ts +20 -0
  42. package/src/types.ts +57 -0
@@ -0,0 +1,548 @@
1
+ import { assertInputObjectType, GraphQLInputObjectType, GraphQLString, GraphQLID, parse, validate } from 'graphql';
2
+ import { g } from '@nixxie-cms/core';
3
+ import { getPasswordFieldKDF } from '@nixxie-cms/core/fields/types/password';
4
+
5
+ const AUTHENTICATION_FAILURE$1 = {
6
+ code: 'FAILURE',
7
+ message: 'Authentication failed.'
8
+ };
9
+ function getBaseAuthSchema({
10
+ authGqlNames,
11
+ listKey,
12
+ identityField,
13
+ secretField,
14
+ base
15
+ }) {
16
+ const kdf = getPasswordFieldKDF(base.schema, listKey, secretField);
17
+ if (!kdf) {
18
+ throw new Error(`${listKey}.${secretField} is not a valid password field.`);
19
+ }
20
+ const ItemAuthenticationWithPasswordSuccess = g.object()({
21
+ name: authGqlNames.ItemAuthenticationWithPasswordSuccess,
22
+ fields: {
23
+ sessionToken: g.field({
24
+ type: g.nonNull(g.String)
25
+ }),
26
+ item: g.field({
27
+ type: g.nonNull(base.object(listKey))
28
+ })
29
+ }
30
+ });
31
+ const ItemAuthenticationWithPasswordFailure = g.object()({
32
+ name: authGqlNames.ItemAuthenticationWithPasswordFailure,
33
+ fields: {
34
+ message: g.field({
35
+ type: g.nonNull(g.String)
36
+ })
37
+ }
38
+ });
39
+ const AuthenticationResult = g.union({
40
+ name: authGqlNames.ItemAuthenticationWithPasswordResult,
41
+ types: [ItemAuthenticationWithPasswordSuccess, ItemAuthenticationWithPasswordFailure],
42
+ resolveType(val) {
43
+ if ('sessionToken' in val) return authGqlNames.ItemAuthenticationWithPasswordSuccess;
44
+ return authGqlNames.ItemAuthenticationWithPasswordFailure;
45
+ }
46
+ });
47
+ const extension = {
48
+ query: {
49
+ authenticatedItem: g.field({
50
+ type: base.object(listKey),
51
+ resolve(rootVal, args, context) {
52
+ const {
53
+ session
54
+ } = context;
55
+ if (!(session !== null && session !== void 0 && session.itemId)) return null;
56
+ return context.db[listKey].findOne({
57
+ where: {
58
+ id: session.itemId
59
+ }
60
+ });
61
+ }
62
+ })
63
+ },
64
+ mutation: {
65
+ endSession: g.field({
66
+ type: g.nonNull(g.Boolean),
67
+ async resolve(rootVal, args, context) {
68
+ var _context$sessionStrat;
69
+ await ((_context$sessionStrat = context.sessionStrategy) === null || _context$sessionStrat === void 0 ? void 0 : _context$sessionStrat.end({
70
+ context
71
+ }));
72
+ return true;
73
+ }
74
+ }),
75
+ [authGqlNames.authenticateItemWithPassword]: g.field({
76
+ type: AuthenticationResult,
77
+ args: {
78
+ [identityField]: g.arg({
79
+ type: g.nonNull(g.String)
80
+ }),
81
+ [secretField]: g.arg({
82
+ type: g.nonNull(g.String)
83
+ })
84
+ },
85
+ async resolve(rootVal, {
86
+ [identityField]: identity,
87
+ [secretField]: secret
88
+ }, context) {
89
+ if (!context.sessionStrategy) throw new Error('No session strategy on context');
90
+ const item = await context.sudo().db[listKey].findOne({
91
+ where: {
92
+ [identityField]: identity
93
+ }
94
+ });
95
+ if (typeof (item === null || item === void 0 ? void 0 : item[secretField]) !== 'string') {
96
+ await kdf.hash('simulated-password-to-counter-timing-attack');
97
+ return AUTHENTICATION_FAILURE$1;
98
+ }
99
+ const equal = await kdf.compare(secret, item[secretField]);
100
+ if (!equal) return AUTHENTICATION_FAILURE$1;
101
+ const sessionToken = await context.sessionStrategy.start({
102
+ data: {
103
+ listKey,
104
+ itemId: item.id
105
+ },
106
+ context
107
+ });
108
+ if (typeof sessionToken !== 'string' || sessionToken.length === 0) {
109
+ return AUTHENTICATION_FAILURE$1;
110
+ }
111
+ return {
112
+ sessionToken,
113
+ item
114
+ };
115
+ }
116
+ })
117
+ }
118
+ };
119
+ return {
120
+ extension,
121
+ ItemAuthenticationWithPasswordSuccess
122
+ };
123
+ }
124
+
125
+ const AUTHENTICATION_FAILURE = 'Authentication failed.';
126
+ function getInitFirstItemSchema({
127
+ authGqlNames,
128
+ listKey,
129
+ fields,
130
+ defaultItemData,
131
+ graphQLSchema,
132
+ ItemAuthenticationWithPasswordSuccess
133
+ }) {
134
+ const createInputConfig = assertInputObjectType(graphQLSchema.getType(`${listKey}CreateInput`)).toConfig();
135
+ const fieldsSet = new Set(fields);
136
+ const initialCreateInput = new GraphQLInputObjectType({
137
+ ...createInputConfig,
138
+ fields: Object.fromEntries(Object.entries(createInputConfig.fields).filter(([fieldKey]) => fieldsSet.has(fieldKey))),
139
+ name: authGqlNames.CreateInitialInput
140
+ });
141
+ return {
142
+ mutation: {
143
+ [authGqlNames.createInitialItem]: g.field({
144
+ type: g.nonNull(ItemAuthenticationWithPasswordSuccess),
145
+ args: {
146
+ data: g.arg({
147
+ type: g.nonNull(initialCreateInput)
148
+ })
149
+ },
150
+ async resolve(rootVal, {
151
+ data
152
+ }, context) {
153
+ if (!context.sessionStrategy) throw new Error('No session strategy on context');
154
+ const sudoContext = context.sudo();
155
+
156
+ // should approximate hasInitFirstItemConditions
157
+ const count = await sudoContext.db[listKey].count();
158
+ if (count !== 0) throw AUTHENTICATION_FAILURE;
159
+
160
+ // Update system state
161
+ // this is strictly speaking incorrect. the db API will do GraphQL coercion on a value which has already been coerced
162
+ // (this is also mostly fine, the chance that people are using things where
163
+ // the input value can't round-trip like the Upload scalar here is quite low)
164
+ const item = await sudoContext.db[listKey].createOne({
165
+ data: {
166
+ ...defaultItemData,
167
+ ...data
168
+ }
169
+ });
170
+ const sessionToken = await context.sessionStrategy.start({
171
+ data: {
172
+ listKey,
173
+ itemId: item.id
174
+ },
175
+ context
176
+ });
177
+ if (typeof sessionToken !== 'string' || sessionToken.length === 0) {
178
+ throw AUTHENTICATION_FAILURE;
179
+ }
180
+ return {
181
+ sessionToken,
182
+ item
183
+ };
184
+ }
185
+ })
186
+ }
187
+ };
188
+ }
189
+
190
+ const getSchemaExtension = ({
191
+ authGqlNames,
192
+ listKey,
193
+ identityField,
194
+ secretField,
195
+ initFirstItem,
196
+ sessionData
197
+ }) => g.extend(base => {
198
+ const uniqueWhereInputType = assertInputObjectType(base.schema.getType(authGqlNames.whereUniqueInputName));
199
+ const identityFieldOnUniqueWhere = uniqueWhereInputType.getFields()[identityField];
200
+ if (base.schema.extensions.sudo && (identityFieldOnUniqueWhere === null || identityFieldOnUniqueWhere === void 0 ? void 0 : identityFieldOnUniqueWhere.type) !== GraphQLString && (identityFieldOnUniqueWhere === null || identityFieldOnUniqueWhere === void 0 ? void 0 : identityFieldOnUniqueWhere.type) !== GraphQLID) {
201
+ throw new Error(`createAuth was called with an identityField of ${identityField} on the list ${listKey} ` + `but that field doesn't allow being searched uniquely with a String or ID. ` + `You should likely add \`isIndexed: 'unique'\` ` + `to the field at ${listKey}.${identityField}`);
202
+ }
203
+ const baseSchema = getBaseAuthSchema({
204
+ authGqlNames: authGqlNames,
205
+ identityField,
206
+ listKey,
207
+ secretField,
208
+ base
209
+ });
210
+
211
+ // technically this will incorrectly error if someone has a schema extension that adds a field to the list output type
212
+ // and then wants to fetch that field with `sessionData` but it's extremely unlikely someone will do that since if
213
+ // they want to add a GraphQL field, they'll probably use a virtual field
214
+ const query = `query($id: ID!) { ${authGqlNames.itemQueryName}(where: { id: $id }) { ${sessionData} } }`;
215
+ let ast;
216
+ try {
217
+ ast = parse(query);
218
+ } catch (err) {
219
+ throw new Error(`The query to get session data has a syntax error, the sessionData option in your createAuth usage is likely incorrect\n${err}`);
220
+ }
221
+ const errors = validate(base.schema, ast);
222
+ if (errors.length) {
223
+ throw new Error(`The query to get session data has validation errors, the sessionData option in your createAuth usage is likely incorrect\n${errors.join('\n')}`);
224
+ }
225
+ return [baseSchema.extension, initFirstItem && getInitFirstItemSchema({
226
+ authGqlNames,
227
+ listKey,
228
+ fields: initFirstItem.fields,
229
+ defaultItemData: initFirstItem.itemData,
230
+ graphQLSchema: base.schema,
231
+ ItemAuthenticationWithPasswordSuccess: baseSchema.ItemAuthenticationWithPasswordSuccess
232
+ })].filter(x => x !== undefined);
233
+ });
234
+
235
+ function configTemplate ({
236
+ labelField
237
+ }) {
238
+ return `import { type AdminConfig } from '@nixxie-cms/core/types'
239
+ import makeNavigation from '@nixxie-cms/auth/components/Navigation'
240
+
241
+ export const components: AdminConfig['components'] = {
242
+ Navigation: makeNavigation({ labelField: '${labelField}' }),
243
+ }
244
+ `;
245
+ }
246
+
247
+ function signinTemplate ({
248
+ authGqlNames,
249
+ identityField,
250
+ secretField
251
+ }) {
252
+ return `import makeSigninPage from '@nixxie-cms/auth/pages/SigninPage'
253
+
254
+ export default makeSigninPage(${JSON.stringify({
255
+ authGqlNames,
256
+ identityField,
257
+ secretField
258
+ })})
259
+ `;
260
+ }
261
+
262
+ function initTemplate ({
263
+ authGqlNames,
264
+ listKey,
265
+ initFirstItem
266
+ }) {
267
+ return `import makeInitPage from '@nixxie-cms/auth/pages/InitPage'
268
+
269
+ export default makeInitPage(${JSON.stringify({
270
+ listKey,
271
+ authGqlNames,
272
+ fieldPaths: initFirstItem.fields,
273
+ enableWelcome: !initFirstItem.skipNixxieWelcome
274
+ })})
275
+ `;
276
+ }
277
+
278
+ function getAuthGqlNames(singular) {
279
+ const lowerSingularName = singular.charAt(0).toLowerCase() + singular.slice(1);
280
+ return {
281
+ itemQueryName: lowerSingularName,
282
+ whereUniqueInputName: `${singular}WhereUniqueInput`,
283
+ authenticateItemWithPassword: `authenticate${singular}WithPassword`,
284
+ ItemAuthenticationWithPasswordResult: `${singular}AuthenticationWithPasswordResult`,
285
+ ItemAuthenticationWithPasswordSuccess: `${singular}AuthenticationWithPasswordSuccess`,
286
+ ItemAuthenticationWithPasswordFailure: `${singular}AuthenticationWithPasswordFailure`,
287
+ CreateInitialInput: `CreateInitial${singular}Input`,
288
+ createInitialItem: `createInitial${singular}`
289
+ };
290
+ }
291
+
292
+ // TODO: use TypeInfo and listKey for types
293
+ /**
294
+ * createAuth function
295
+ *
296
+ * Generates config for Nixxie to implement standard auth features.
297
+ */
298
+ function createAuth({
299
+ listKey,
300
+ secretField,
301
+ initFirstItem,
302
+ identityField,
303
+ sessionData = 'id'
304
+ }) {
305
+ /**
306
+ * getAdditionalFiles
307
+ *
308
+ * This function adds files to be generated into the Admin UI build. Must be added to the
309
+ * ui.getAdditionalFiles config.
310
+ *
311
+ * The signin page is always included, and the init page is included when initFirstItem is set
312
+ */
313
+ const authGetAdditionalFiles = config => {
314
+ var _listConfig$ui$labelF, _listConfig$ui, _listConfig$graphql$s, _listConfig$graphql;
315
+ // TODO: FIXME: this is a duplication of initialise-lists:747
316
+ const listConfig = config.lists[listKey];
317
+ const labelField = (_listConfig$ui$labelF = (_listConfig$ui = listConfig.ui) === null || _listConfig$ui === void 0 ? void 0 : _listConfig$ui.labelField) !== null && _listConfig$ui$labelF !== void 0 ? _listConfig$ui$labelF : listConfig.fields.label ? 'label' : listConfig.fields.name ? 'name' : listConfig.fields.title ? 'title' : 'id';
318
+ const authGqlNames = getAuthGqlNames((_listConfig$graphql$s = (_listConfig$graphql = listConfig.graphql) === null || _listConfig$graphql === void 0 ? void 0 : _listConfig$graphql.singular) !== null && _listConfig$graphql$s !== void 0 ? _listConfig$graphql$s : listKey);
319
+ const filesToWrite = [{
320
+ mode: 'write',
321
+ src: signinTemplate({
322
+ authGqlNames,
323
+ identityField,
324
+ secretField
325
+ }),
326
+ outputPath: 'pages/signin.js'
327
+ }, {
328
+ mode: 'write',
329
+ src: configTemplate({
330
+ labelField
331
+ }),
332
+ outputPath: 'config.ts'
333
+ }];
334
+ if (initFirstItem) {
335
+ filesToWrite.push({
336
+ mode: 'write',
337
+ src: initTemplate({
338
+ authGqlNames,
339
+ listKey,
340
+ initFirstItem
341
+ }),
342
+ outputPath: 'pages/init.js'
343
+ });
344
+ }
345
+ return filesToWrite;
346
+ };
347
+ function throwIfInvalidConfig(config) {
348
+ if (!(listKey in config.lists)) {
349
+ throw new Error(`withAuth cannot find the list "${listKey}"`);
350
+ }
351
+
352
+ // TODO: verify that the identity field is unique
353
+ // TODO: verify that the field is required
354
+ const list = config.lists[listKey];
355
+ if (!(identityField in list.fields)) {
356
+ throw new Error(`withAuth cannot find the identity field "${listKey}.${identityField}"`);
357
+ }
358
+ if (!(secretField in list.fields)) {
359
+ throw new Error(`withAuth cannot find the secret field "${listKey}.${secretField}"`);
360
+ }
361
+ for (const fieldKey of (initFirstItem === null || initFirstItem === void 0 ? void 0 : initFirstItem.fields) || []) {
362
+ if (fieldKey in list.fields) continue;
363
+ throw new Error(`initFirstItem.fields has unknown field "${listKey}.${fieldKey}"`);
364
+ }
365
+ }
366
+
367
+ // this strategy wraps the existing session strategy,
368
+ // and injects the requested session.data before returning
369
+ function authSessionStrategy(_sessionStrategy) {
370
+ const {
371
+ get,
372
+ ...sessionStrategy
373
+ } = _sessionStrategy;
374
+ return {
375
+ ...sessionStrategy,
376
+ get: async ({
377
+ context
378
+ }) => {
379
+ const session = await get({
380
+ context
381
+ });
382
+ const sudoContext = context.sudo();
383
+ if (!(session !== null && session !== void 0 && session.itemId)) return;
384
+
385
+ // TODO: replace with SessionSecret: HMAC({ listKey, identityField, secretField }, SessionSecretVar)
386
+ // if (session.listKey !== listKey) return null
387
+
388
+ try {
389
+ const data = await sudoContext.query[listKey].findOne({
390
+ where: {
391
+ id: session.itemId
392
+ },
393
+ query: sessionData
394
+ });
395
+ if (!data) return;
396
+ return {
397
+ ...session,
398
+ itemId: session.itemId,
399
+ data
400
+ };
401
+ } catch (e) {
402
+ console.error(e);
403
+ // WARNING: this is probably an invalid configuration
404
+ return;
405
+ }
406
+ }
407
+ };
408
+ }
409
+ async function hasInitFirstItemConditions(context) {
410
+ // do nothing if they aren't using this feature
411
+ if (!initFirstItem) return false;
412
+
413
+ // if they have a session, there is no initialisation necessary
414
+ if (context.session) return false;
415
+ const count = await context.sudo().db[listKey].count({});
416
+ return count === 0;
417
+ }
418
+ async function authMiddleware({
419
+ context,
420
+ wasAccessAllowed,
421
+ basePath
422
+ }) {
423
+ const {
424
+ req
425
+ } = context;
426
+ const {
427
+ pathname
428
+ } = new URL(req.url, 'http://_');
429
+
430
+ // redirect to init if initFirstItem conditions are met
431
+ if (pathname !== `${basePath}/init` && (await hasInitFirstItemConditions(context))) {
432
+ return {
433
+ kind: 'redirect',
434
+ to: `${basePath}/init`
435
+ };
436
+ }
437
+
438
+ // redirect to / if attempting to /init and initFirstItem conditions are not met
439
+ if (pathname === `${basePath}/init` && !(await hasInitFirstItemConditions(context))) {
440
+ return {
441
+ kind: 'redirect',
442
+ to: basePath
443
+ };
444
+ }
445
+
446
+ // don't redirect if we have access
447
+ if (wasAccessAllowed) return;
448
+
449
+ // otherwise, redirect to signin
450
+ return {
451
+ kind: 'redirect',
452
+ to: `${basePath}/signin`
453
+ };
454
+ }
455
+ function defaultIsAccessAllowed({
456
+ session
457
+ }) {
458
+ return session !== undefined;
459
+ }
460
+ function defaultExtendGraphqlSchema(schema) {
461
+ return schema;
462
+ }
463
+
464
+ /**
465
+ * withAuth
466
+ *
467
+ * Automatically extends your configuration with a prescriptive implementation.
468
+ */
469
+ function withAuth(config) {
470
+ var _ui, _listConfig$graphql$s2, _listConfig$graphql2;
471
+ throwIfInvalidConfig(config);
472
+ let {
473
+ ui
474
+ } = config;
475
+ if (!((_ui = ui) !== null && _ui !== void 0 && _ui.isDisabled)) {
476
+ var _ui$basePath, _ui2;
477
+ const {
478
+ getAdditionalFiles = () => [],
479
+ isAccessAllowed = defaultIsAccessAllowed,
480
+ pageMiddleware,
481
+ publicPages = []
482
+ } = ui || {};
483
+ const authPublicPages = [`${(_ui$basePath = (_ui2 = ui) === null || _ui2 === void 0 ? void 0 : _ui2.basePath) !== null && _ui$basePath !== void 0 ? _ui$basePath : ''}/signin`];
484
+ ui = {
485
+ ...ui,
486
+ publicPages: [...publicPages, ...authPublicPages],
487
+ getAdditionalFiles: async () => [...(await getAdditionalFiles()), ...authGetAdditionalFiles(config)],
488
+ isAccessAllowed: async context => {
489
+ if (await hasInitFirstItemConditions(context)) return true;
490
+ return isAccessAllowed(context);
491
+ },
492
+ pageMiddleware: async args => {
493
+ const shouldRedirect = await authMiddleware(args);
494
+ if (shouldRedirect) return shouldRedirect;
495
+ return pageMiddleware === null || pageMiddleware === void 0 ? void 0 : pageMiddleware(args);
496
+ }
497
+ };
498
+ }
499
+ if (!config.session) throw new TypeError('Missing .session configuration');
500
+ const {
501
+ graphql
502
+ } = config;
503
+ const {
504
+ extendGraphqlSchema = defaultExtendGraphqlSchema
505
+ } = graphql !== null && graphql !== void 0 ? graphql : {};
506
+ const listConfig = config.lists[listKey];
507
+
508
+ /**
509
+ * extendGraphqlSchema
510
+ *
511
+ * Must be added to the extendGraphqlSchema config. Can be composed.
512
+ */
513
+ const authGqlNames = getAuthGqlNames((_listConfig$graphql$s2 = (_listConfig$graphql2 = listConfig.graphql) === null || _listConfig$graphql2 === void 0 ? void 0 : _listConfig$graphql2.singular) !== null && _listConfig$graphql$s2 !== void 0 ? _listConfig$graphql$s2 : listKey);
514
+ const authExtendGraphqlSchema = getSchemaExtension({
515
+ authGqlNames,
516
+ listKey,
517
+ identityField,
518
+ secretField,
519
+ initFirstItem,
520
+ sessionData
521
+ });
522
+ return {
523
+ ...config,
524
+ graphql: {
525
+ ...config.graphql,
526
+ extendGraphqlSchema: schema => {
527
+ return extendGraphqlSchema(authExtendGraphqlSchema(schema));
528
+ }
529
+ },
530
+ ui,
531
+ session: authSessionStrategy(config.session),
532
+ lists: {
533
+ ...config.lists,
534
+ [listKey]: {
535
+ ...listConfig,
536
+ fields: {
537
+ ...listConfig.fields
538
+ }
539
+ }
540
+ }
541
+ };
542
+ }
543
+ return {
544
+ withAuth
545
+ };
546
+ }
547
+
548
+ export { createAuth };
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var router = require('@nixxie-cms/core/admin-ui/router');
5
+
6
+ /**
7
+ * Returns a safe, site-relative path to redirect to after authentication.
8
+ *
9
+ * Reads the `from` query parameter and only honours it when it is a
10
+ * site-relative path (starts with a single `/`). Absolute URLs and
11
+ * protocol-relative URLs (`//evil.com`) are rejected to prevent open
12
+ * redirects; in those cases it falls back to `/`.
13
+ */
14
+ function useRedirect() {
15
+ const router$1 = router.useRouter();
16
+ const raw = router$1.query.from;
17
+ const from = Array.isArray(raw) ? raw[0] : raw;
18
+ return react.useMemo(() => {
19
+ if (typeof from === 'string' && from.startsWith('/') && !from.startsWith('//')) {
20
+ return from;
21
+ }
22
+ return '/';
23
+ }, [from]);
24
+ }
25
+
26
+ exports.useRedirect = useRedirect;
@@ -0,0 +1,24 @@
1
+ import { useMemo } from 'react';
2
+ import { useRouter } from '@nixxie-cms/core/admin-ui/router';
3
+
4
+ /**
5
+ * Returns a safe, site-relative path to redirect to after authentication.
6
+ *
7
+ * Reads the `from` query parameter and only honours it when it is a
8
+ * site-relative path (starts with a single `/`). Absolute URLs and
9
+ * protocol-relative URLs (`//evil.com`) are rejected to prevent open
10
+ * redirects; in those cases it falls back to `/`.
11
+ */
12
+ function useRedirect() {
13
+ const router = useRouter();
14
+ const raw = router.query.from;
15
+ const from = Array.isArray(raw) ? raw[0] : raw;
16
+ return useMemo(() => {
17
+ if (typeof from === 'string' && from.startsWith('/') && !from.startsWith('//')) {
18
+ return from;
19
+ }
20
+ return '/';
21
+ }, [from]);
22
+ }
23
+
24
+ export { useRedirect as u };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@nixxie-cms/auth",
3
+ "version": "1.0.1",
4
+ "license": "MIT",
5
+ "main": "dist/nixxie-cms-auth.cjs.js",
6
+ "module": "dist/nixxie-cms-auth.esm.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/nixxie-cms-auth.cjs.js",
10
+ "module": "./dist/nixxie-cms-auth.esm.js",
11
+ "default": "./dist/nixxie-cms-auth.cjs.js"
12
+ },
13
+ "./pages/InitPage": {
14
+ "types": "./pages/InitPage/dist/nixxie-cms-auth-pages-InitPage.cjs.js",
15
+ "module": "./pages/InitPage/dist/nixxie-cms-auth-pages-InitPage.esm.js",
16
+ "default": "./pages/InitPage/dist/nixxie-cms-auth-pages-InitPage.cjs.js"
17
+ },
18
+ "./pages/SigninPage": {
19
+ "types": "./pages/SigninPage/dist/nixxie-cms-auth-pages-SigninPage.cjs.js",
20
+ "module": "./pages/SigninPage/dist/nixxie-cms-auth-pages-SigninPage.esm.js",
21
+ "default": "./pages/SigninPage/dist/nixxie-cms-auth-pages-SigninPage.cjs.js"
22
+ },
23
+ "./components/Navigation": {
24
+ "types": "./components/Navigation/dist/nixxie-cms-auth-components-Navigation.cjs.js",
25
+ "module": "./components/Navigation/dist/nixxie-cms-auth-components-Navigation.esm.js",
26
+ "default": "./components/Navigation/dist/nixxie-cms-auth-components-Navigation.cjs.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "dependencies": {
31
+ "@babel/runtime": "^7.24.7",
32
+ "@keystar/ui": "^0.7.21",
33
+ "fast-deep-equal": "^3.1.3",
34
+ "graphql": "^16.8.1",
35
+ "next": "^15.5.12"
36
+ },
37
+ "devDependencies": {
38
+ "react": "^19.2.4",
39
+ "@nixxie-cms/core": "^1.0.1"
40
+ },
41
+ "peerDependencies": {
42
+ "@nixxie-cms/core": "^1.0.1",
43
+ "react": "^19.2.4"
44
+ },
45
+ "preconstruct": {
46
+ "entrypoints": [
47
+ "index.ts",
48
+ "components/Navigation.tsx",
49
+ "pages/*.tsx"
50
+ ]
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/nixxiecms/nixxie/tree/main/packages/auth"
55
+ }
56
+ }
@@ -0,0 +1,3 @@
1
+ export * from "../../../dist/declarations/src/pages/InitPage.js";
2
+ export { default } from "../../../dist/declarations/src/pages/InitPage.js";
3
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibml4eGllLWNtcy1hdXRoLXBhZ2VzLUluaXRQYWdlLmNqcy5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vZGlzdC9kZWNsYXJhdGlvbnMvc3JjL3BhZ2VzL0luaXRQYWdlLmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEifQ==