@opensaas/keystone-nextjs-auth 20.5.0 → 21.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,43 +1,74 @@
1
- import NextAuth from 'next-auth';
1
+ import NextAuth, {
2
+ CookiesOptions,
3
+ EventCallbacks,
4
+ PagesOptions,
5
+ } from 'next-auth';
2
6
  import type { KeystoneListsAPI } from '@keystone-6/core/types';
3
7
  import { Provider } from 'next-auth/providers';
8
+ import { JWTOptions } from 'next-auth/jwt';
4
9
  import { validateNextAuth } from '../lib/validateNextAuth';
5
10
 
6
- // Need to bring in correct props
7
- type NextAuthPageProps = {
11
+ // TODO: See if possible to merge with `type AuthConfig`
12
+ type CoreNextAuthPageProps = {
13
+ autoCreate: boolean;
14
+ cookies?: Partial<CookiesOptions>;
15
+ events?: Partial<EventCallbacks>;
8
16
  identityField: string;
9
- mutationName: string;
10
- providers: Provider[];
11
- query: KeystoneListsAPI<any>;
12
- sessionData: string;
17
+ jwt?: Partial<JWTOptions>;
13
18
  listKey: string;
14
- autoCreate: boolean;
15
- userMap: any;
16
- accountMap: any;
17
- profileMap: any;
19
+ pages?: Partial<PagesOptions>;
20
+ providers?: Provider[];
21
+ resolver?: Function | undefined;
22
+ sessionData: string | undefined;
18
23
  sessionSecret: string;
19
24
  };
20
25
 
26
+ type NextAuthGglProps = {
27
+ mutationName?: string;
28
+ query?: KeystoneListsAPI<any>;
29
+ };
30
+
31
+ export type NextAuthPageProps = CoreNextAuthPageProps & NextAuthGglProps;
32
+
21
33
  export default function NextAuthPage(props: NextAuthPageProps) {
22
34
  const {
35
+ autoCreate,
36
+ cookies,
37
+ events,
38
+ identityField,
39
+ jwt,
40
+ listKey,
41
+ pages,
23
42
  providers,
24
43
  query,
25
- identityField,
44
+ resolver,
26
45
  sessionData,
27
- listKey,
28
- autoCreate,
29
- userMap,
30
- accountMap,
31
- profileMap,
32
46
  sessionSecret,
33
47
  } = props;
48
+ // TODO: (v1.1). https://github.com/ijsto/keystone-6-oauth/projects/1#card-78602004
49
+ console.log('NextAuthPages... ', pages);
50
+
51
+ if (!query) {
52
+ console.error('NextAuthPage got no query.');
53
+ return null;
54
+ }
55
+
56
+ if (!providers || !providers.length) {
57
+ console.error('You need to provide at least one provider.');
58
+ return null;
59
+ }
60
+
34
61
  const list = query[listKey];
35
62
  const queryAPI = query[listKey];
36
63
  const protectIdentities = true;
37
64
 
38
65
  return NextAuth({
39
- secret: sessionSecret,
66
+ cookies,
40
67
  providers,
68
+ pages: pages || {},
69
+ events: events || {},
70
+ jwt: jwt || {},
71
+ secret: sessionSecret,
41
72
  callbacks: {
42
73
  async signIn({ user, account, profile }) {
43
74
  let identity;
@@ -48,31 +79,21 @@ export default function NextAuthPage(props: NextAuthPageProps) {
48
79
  } else {
49
80
  identity = 0;
50
81
  }
82
+ const userInput = resolver
83
+ ? await resolver({ user, account, profile })
84
+ : {};
85
+
51
86
  const result = await validateNextAuth(
52
87
  identityField,
53
88
  identity,
54
89
  protectIdentities,
55
90
  queryAPI
56
91
  );
57
- const data: any = {};
58
- // eslint-disable-next-line no-restricted-syntax
59
- for (const key in userMap) {
60
- if (Object.prototype.hasOwnProperty.call(userMap, key)) {
61
- data[key] = user[userMap[key]];
62
- }
63
- }
64
- // eslint-disable-next-line no-restricted-syntax
65
- for (const key in accountMap) {
66
- if (Object.prototype.hasOwnProperty.call(accountMap, key)) {
67
- data[key] = account[accountMap[key]];
68
- }
69
- }
70
- // eslint-disable-next-line no-restricted-syntax
71
- for (const key in profileMap) {
72
- if (Object.prototype.hasOwnProperty.call(profileMap, key)) {
73
- data[key] = profile[profileMap[key]];
74
- }
75
- }
92
+ // ID
93
+ const data: any = {
94
+ [identityField]: identity,
95
+ ...userInput,
96
+ };
76
97
 
77
98
  if (!result.success) {
78
99
  if (!autoCreate) {
@@ -125,7 +146,7 @@ export default function NextAuthPage(props: NextAuthPageProps) {
125
146
  );
126
147
 
127
148
  if (!result.success) {
128
- return { result: false };
149
+ return token;
129
150
  }
130
151
  token.itemId = result.item.id;
131
152
  }
package/src/schema.ts CHANGED
@@ -1,39 +1,17 @@
1
1
  import { ExtendGraphqlSchema } from '@keystone-6/core/types';
2
2
 
3
- import { assertInputObjectType, GraphQLString, GraphQLID } from 'graphql';
4
3
  import { graphql } from '@keystone-6/core';
5
- import { AuthGqlNames } from './types';
6
4
  import { getBaseAuthSchema } from './gql/getBaseAuthSchema';
7
5
 
8
6
  export const getSchemaExtension = ({
9
- identityField,
10
7
  listKey,
11
- gqlNames,
12
8
  }: {
13
9
  identityField: string;
14
10
  listKey: string;
15
- gqlNames: AuthGqlNames;
16
11
  }): ExtendGraphqlSchema =>
17
12
  graphql.extend((base) => {
18
- const uniqueWhereInputType = assertInputObjectType(
19
- base.schema.getType(`${listKey}WhereUniqueInput`)
20
- );
21
- const identityFieldOnUniqueWhere =
22
- uniqueWhereInputType.getFields()[identityField];
23
- if (
24
- identityFieldOnUniqueWhere?.type !== GraphQLString &&
25
- identityFieldOnUniqueWhere?.type !== GraphQLID
26
- ) {
27
- throw new Error(
28
- `createAuth was called with an identityField of ${identityField} on the list ${listKey} ` +
29
- `but that field doesn't allow being searched uniquely with a String or ID. ` +
30
- `You should likely add \`isIndexed: 'unique'\` ` +
31
- `to the field at ${listKey}.${identityField}`
32
- );
33
- }
34
13
  const baseSchema = getBaseAuthSchema({
35
14
  listKey,
36
- gqlNames,
37
15
  base,
38
16
  });
39
17
 
@@ -1,5 +1,5 @@
1
1
  import ejs from 'ejs';
2
- import { AuthGqlNames } from '../types';
2
+ import { NextAuthPageProps } from '../pages/NextAuthPage';
3
3
 
4
4
  const template = `
5
5
  import getNextAuthPage from '@opensaas/keystone-nextjs-auth/pages/NextAuthPage';
@@ -7,49 +7,32 @@ import { query } from '.keystone/api';
7
7
  import keystoneConfig from '../../../../../keystone';
8
8
 
9
9
  export default getNextAuthPage({
10
+ autoCreate: <%= autoCreate %>,
10
11
  identityField: '<%= identityField %>',
11
- sessionData: '<%= sessionData %>',
12
12
  listKey: '<%= listKey %>',
13
- userMap: <%- JSON.stringify(userMap) %>,
14
- accountMap: <%- JSON.stringify(accountMap) %>,
15
- profileMap: <%- JSON.stringify(profileMap) %>,
16
- autoCreate: <%= autoCreate %>,
17
- sessionSecret: '<%= sessionSecret %>',
13
+ pages: keystoneConfig.pages,
18
14
  providers: keystoneConfig.providers,
19
15
  query,
16
+ resolver: keystoneConfig.resolver,
17
+ sessionData: '<%= sessionData %>',
18
+ sessionSecret: '<%= sessionSecret %>',
20
19
  });
21
20
  `;
22
21
 
22
+ type AuthTemplateOptions = NextAuthPageProps;
23
+
23
24
  export const authTemplate = ({
24
- gqlNames,
25
+ autoCreate,
25
26
  identityField,
26
- sessionData,
27
27
  listKey,
28
- autoCreate,
29
- userMap,
30
- accountMap,
31
- profileMap,
28
+ sessionData,
32
29
  sessionSecret,
33
- }: {
34
- gqlNames: AuthGqlNames;
35
- identityField: string;
36
- sessionData: any;
37
- listKey: string;
38
- autoCreate: boolean;
39
- userMap: any;
40
- accountMap: any;
41
- profileMap: any;
42
- sessionSecret: string;
43
- }) => {
30
+ }: AuthTemplateOptions) => {
44
31
  const authOut = ejs.render(template, {
45
- gqlNames,
46
32
  identityField,
47
33
  sessionData,
48
34
  listKey,
49
35
  autoCreate,
50
- userMap,
51
- accountMap,
52
- profileMap,
53
36
  sessionSecret,
54
37
  });
55
38
  return authOut;
@@ -9,6 +9,9 @@ module.exports = withPreconstruct({
9
9
  typescript: {
10
10
  ignoreBuildErrors: true,
11
11
  },
12
+ env: {
13
+ NEXTAUTH_URL: process.env.NEXTAUTH_URL || 'http://localhost:<%= process.env.PORT || 3000 %><%= keystonePath || '' %>/api/auth',
14
+ },
12
15
  eslint: {
13
16
  ignoreDuringBuilds: true,
14
17
  },
package/src/types.ts CHANGED
@@ -1,44 +1,43 @@
1
1
  import { BaseListTypeInfo, KeystoneConfig } from '@keystone-6/core/types';
2
+ import { CookiesOptions, PagesOptions } from 'next-auth';
2
3
  import { Provider } from 'next-auth/providers';
3
4
 
4
- export type AuthGqlNames = {
5
- CreateInitialInput: string;
6
- createInitialItem: string;
7
- authenticateItemWithPassword: string;
8
- ItemAuthenticationWithPasswordResult: string;
9
- ItemAuthenticationWithPasswordSuccess: string;
10
- ItemAuthenticationWithPasswordFailure: string;
11
- };
12
-
13
5
  export type NextAuthSession = { listKey: string; itemId: string; data: any };
14
6
 
15
7
  export type NextAuthProviders = Provider[];
16
8
 
17
- type KeytoneAuthProviders = {
9
+ type KeytoneOAuthOptions = {
18
10
  providers: NextAuthProviders;
11
+ pages?: Partial<PagesOptions>;
12
+ };
13
+ type NextAuthOptions = {
14
+ cookies?: Partial<CookiesOptions>;
15
+ resolver: any;
19
16
  };
20
17
 
21
- export type KeystoneAuthConfig = KeystoneConfig & KeytoneAuthProviders;
18
+ export type KeystoneOAuthConfig = KeystoneConfig &
19
+ KeytoneOAuthOptions &
20
+ NextAuthOptions;
22
21
 
23
22
  export type AuthConfig<GeneratedListTypes extends BaseListTypeInfo> = {
23
+ /** Auth Create users in Keystone DB from Auth Provider */
24
+ autoCreate: boolean;
25
+ /** Adds ability to customize cookie options, for example, to facilitate cross-subdomain functionality */
26
+ cookies?: Partial<CookiesOptions>;
24
27
  /** The key of the list to authenticate users with */
25
28
  listKey: GeneratedListTypes['key'];
26
29
  /** The path of the field the identity is stored in; must be text-ish */
27
30
  identityField: GeneratedListTypes['fields'];
28
- /** Session data population */
29
- sessionData?: string;
30
- /** Auth Create users in Keystone DB from Auth Provider */
31
- autoCreate: boolean;
32
- /** Map User in next-auth to item */
33
- userMap: any;
34
- /** Map Account in next-auth to item */
35
- accountMap: any;
36
- /** Map Profile in next-auth to item */
37
- profileMap: any;
38
31
  /** Path for Keystone interface */
39
32
  keystonePath?: string;
33
+ // Custom pages for different NextAuth events
34
+ pages?: any; // TODO: Fix types
40
35
  /** Providers for Next Auth */
41
36
  providers: NextAuthProviders;
37
+ /** Resolver for user to define their profile */
38
+ resolver?: Function | undefined;
39
+ /** Session data population */
40
+ sessionData?: string | undefined;
42
41
  /** Next-Auth Session Secret */
43
42
  sessionSecret: string;
44
43
  };
@@ -1,81 +0,0 @@
1
- import type {
2
- GraphQLSchemaExtension,
3
- KeystoneContext,
4
- } from '@keystone-6/core/types';
5
- import {
6
- assertInputObjectType,
7
- GraphQLInputObjectType,
8
- GraphQLSchema,
9
- printType,
10
- } from 'graphql';
11
-
12
- import { AuthGqlNames, InitFirstItemConfig } from '../types';
13
-
14
- export function getInitFirstItemSchema({
15
- listKey,
16
- fields,
17
- itemData,
18
- gqlNames,
19
- graphQLSchema,
20
- }: {
21
- listKey: string;
22
- fields: InitFirstItemConfig<any>['fields'];
23
- itemData: InitFirstItemConfig<any>['itemData'];
24
- gqlNames: AuthGqlNames;
25
- graphQLSchema: GraphQLSchema;
26
- }): GraphQLSchemaExtension {
27
- const createInputConfig = assertInputObjectType(
28
- graphQLSchema.getType(`${listKey}CreateInput`)
29
- ).toConfig();
30
- const fieldsSet = new Set<any>(fields);
31
- const initialCreateInput = printType(
32
- new GraphQLInputObjectType({
33
- ...createInputConfig,
34
- fields: Object.fromEntries(
35
- Object.entries(createInputConfig.fields).filter(([fieldKey]) =>
36
- fieldsSet.has(fieldKey)
37
- )
38
- ),
39
- name: gqlNames.CreateInitialInput,
40
- })
41
- );
42
- return {
43
- typeDefs: `
44
- ${initialCreateInput}
45
- type Mutation {
46
- ${gqlNames.createInitialItem}(data: ${gqlNames.CreateInitialInput}!): ${gqlNames.ItemAuthenticationWithPasswordSuccess}!
47
- }
48
- `,
49
- resolvers: {
50
- Mutation: {
51
- async [gqlNames.createInitialItem](
52
- root: any,
53
- { data }: { data: Record<string, any> },
54
- context: KeystoneContext
55
- ) {
56
- if (!context.startSession) {
57
- throw new Error('No session implementation available on context');
58
- }
59
-
60
- const dbItemAPI = context.sudo().db[listKey];
61
- const count = await dbItemAPI.count({});
62
- if (count !== 0) {
63
- throw new Error(
64
- 'Initial items can only be created when no items exist in that list'
65
- );
66
- }
67
-
68
- // Update system state
69
- const item = await dbItemAPI.createOne({
70
- data: { ...data, ...itemData },
71
- });
72
- const sessionToken = await context.startSession({
73
- listKey,
74
- itemId: item.id,
75
- });
76
- return { item, sessionToken };
77
- },
78
- },
79
- },
80
- };
81
- }