From 1195f9b7afa4c79934694c82b75ddf15718f631e Mon Sep 17 00:00:00 2001 From: Josh Calder Date: Fri, 19 May 2023 18:29:21 +1000 Subject: [PATCH 01/21] add example/custom-session-next-auth --- .../admin/pages/api/auth/[...nextauth].ts | 17 ++ examples/custom-session-next-auth/keystone.ts | 69 ++++++ .../custom-session-next-auth/package.json | 21 ++ .../sandbox.config.json | 7 + .../custom-session-next-auth/schema.graphql | 215 ++++++++++++++++++ .../custom-session-next-auth/schema.prisma | 19 ++ examples/custom-session-next-auth/schema.ts | 27 +++ 7 files changed, 375 insertions(+) create mode 100644 examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts create mode 100644 examples/custom-session-next-auth/keystone.ts create mode 100644 examples/custom-session-next-auth/package.json create mode 100644 examples/custom-session-next-auth/sandbox.config.json create mode 100644 examples/custom-session-next-auth/schema.graphql create mode 100644 examples/custom-session-next-auth/schema.prisma create mode 100644 examples/custom-session-next-auth/schema.ts diff --git a/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts b/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts new file mode 100644 index 00000000000..301384723a6 --- /dev/null +++ b/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts @@ -0,0 +1,17 @@ +import NextAuth from 'next-auth'; +import GithubProvider from 'next-auth/providers/github'; + +// See https://next-auth.js.org/configuration/options for more details on what goes in here +export const authOptions = { + secret: '--DEV--COOKIE--SECRET--CHANGE--ME--==', + // Configure one or more authentication providers + providers: [ + GithubProvider({ + clientId: process.env.GITHUB_ID!, + clientSecret: process.env.GITHUB_SECRET!, + }), + // ...add more providers here + ], +}; + +export default NextAuth(authOptions); diff --git a/examples/custom-session-next-auth/keystone.ts b/examples/custom-session-next-auth/keystone.ts new file mode 100644 index 00000000000..6a61e932e5b --- /dev/null +++ b/examples/custom-session-next-auth/keystone.ts @@ -0,0 +1,69 @@ +import { config } from '@keystone-6/core'; +import { getServerSession } from 'next-auth/next'; +import type { Session } from 'next-auth'; +import { fixPrismaPath } from '../example-utils'; +import { lists } from './schema'; +import { authOptions } from './admin/pages/api/auth/[...nextauth]'; +import type { Context, TypeInfo } from '.keystone/types'; + +// WARNING: this example is for demonstration purposes only +// as with each of our examples, it has not been vetted +// or tested for any particular usage + +const nextAuthSession = { + async get({ context }: { context: Context }): Promise { + const { req, res } = context; + if (!req || !res || !req.headers.cookie) return; + // We need to construct the cookies object so next-auth gets to correct object + const cookies: Record = {}; + req.headers.cookie.split(';').forEach(cookie => { + const [key, value] = cookie.trim().split('='); + cookies[key] = decodeURIComponent(value); + }); + return (await getServerSession({ headers: req.headers, cookies } as any, res, authOptions)) as + | Session + | undefined; + }, + // we don't need these unless we want to support the functions + // context.sessionStrategy.start + // context.sessionStrategy.end + // + async start() {}, + async end() {}, +}; + +export default config({ + db: { + provider: 'sqlite', + url: process.env.DATABASE_URL || 'file:./keystone-example.db', + + // WARNING: this is only needed for our monorepo examples, dont do this + ...fixPrismaPath, + }, + ui: { + // The following API routes are required for NextAuth.js + publicPages: [ + '/api/auth/csrf', + '/api/auth/signin', + '/api/auth/callback', + '/api/auth/session', + '/api/auth/providers', + '/api/auth/signout', + '/api/auth/error', + // Each provider will need a separate callback and signin page listed here + '/api/auth/signin/github', + '/api/auth/callback/github', + ], + // Adding Page middleware ensures that users are redirected to the signin page if they are not signed in. + pageMiddleware: async ({ wasAccessAllowed }) => { + if (wasAccessAllowed) return; + return { + kind: 'redirect', + to: '/api/auth/signin', + }; + }, + }, + lists, + // you can find out more at https://keystonejs.com/docs/apis/session#session-api + session: nextAuthSession, +}); diff --git a/examples/custom-session-next-auth/package.json b/examples/custom-session-next-auth/package.json new file mode 100644 index 00000000000..d7745bc4eda --- /dev/null +++ b/examples/custom-session-next-auth/package.json @@ -0,0 +1,21 @@ +{ + "name": "@keystone-6/custom-session-next-auth", + "version": "0.1.1", + "private": true, + "license": "MIT", + "scripts": { + "dev": "keystone dev", + "start": "keystone start", + "build": "keystone build", + "postinstall": "keystone postinstall" + }, + "dependencies": { + "@keystone-6/core": "^5.0.0", + "@prisma/client": "^4.14.0", + "next-auth": "^4.22.1" + }, + "devDependencies": { + "prisma": "^4.14.0", + "typescript": "~5.0.0" + } +} diff --git a/examples/custom-session-next-auth/sandbox.config.json b/examples/custom-session-next-auth/sandbox.config.json new file mode 100644 index 00000000000..7a34682ee45 --- /dev/null +++ b/examples/custom-session-next-auth/sandbox.config.json @@ -0,0 +1,7 @@ +{ + "template": "node", + "container": { + "startScript": "keystone dev", + "node": "16" + } +} diff --git a/examples/custom-session-next-auth/schema.graphql b/examples/custom-session-next-auth/schema.graphql new file mode 100644 index 00000000000..9975f729501 --- /dev/null +++ b/examples/custom-session-next-auth/schema.graphql @@ -0,0 +1,215 @@ +# This file is automatically generated by Keystone, do not modify it manually. +# Modify your Keystone config when you want to change this. + +type Post { + id: ID! + title: String + content: String +} + +input PostWhereUniqueInput { + id: ID +} + +input PostWhereInput { + AND: [PostWhereInput!] + OR: [PostWhereInput!] + NOT: [PostWhereInput!] + id: IDFilter + title: StringFilter + content: StringFilter +} + +input IDFilter { + equals: ID + in: [ID!] + notIn: [ID!] + lt: ID + lte: ID + gt: ID + gte: ID + not: IDFilter +} + +input StringFilter { + equals: String + in: [String!] + notIn: [String!] + lt: String + lte: String + gt: String + gte: String + contains: String + startsWith: String + endsWith: String + not: NestedStringFilter +} + +input NestedStringFilter { + equals: String + in: [String!] + notIn: [String!] + lt: String + lte: String + gt: String + gte: String + contains: String + startsWith: String + endsWith: String + not: NestedStringFilter +} + +input PostOrderByInput { + id: OrderDirection + title: OrderDirection + content: OrderDirection +} + +enum OrderDirection { + asc + desc +} + +input PostUpdateInput { + title: String + content: String +} + +input PostUpdateArgs { + where: PostWhereUniqueInput! + data: PostUpdateInput! +} + +input PostCreateInput { + title: String + content: String +} + +""" +The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") + +type Mutation { + createPost(data: PostCreateInput!): Post + createPosts(data: [PostCreateInput!]!): [Post] + updatePost(where: PostWhereUniqueInput!, data: PostUpdateInput!): Post + updatePosts(data: [PostUpdateArgs!]!): [Post] + deletePost(where: PostWhereUniqueInput!): Post + deletePosts(where: [PostWhereUniqueInput!]!): [Post] + endSession: Boolean! +} + +type Query { + posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] + post(where: PostWhereUniqueInput!): Post + postsCount(where: PostWhereInput! = {}): Int + keystone: KeystoneMeta! +} + +type KeystoneMeta { + adminMeta: KeystoneAdminMeta! +} + +type KeystoneAdminMeta { + lists: [KeystoneAdminUIListMeta!]! + list(key: String!): KeystoneAdminUIListMeta +} + +type KeystoneAdminUIListMeta { + key: String! + itemQueryName: String! + listQueryName: String! + hideCreate: Boolean! + hideDelete: Boolean! + path: String! + label: String! + singular: String! + plural: String! + description: String + initialColumns: [String!]! + pageSize: Int! + labelField: String! + fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! + initialSort: KeystoneAdminUISort + isHidden: Boolean! + isSingleton: Boolean! +} + +type KeystoneAdminUIFieldMeta { + path: String! + label: String! + description: String + isOrderable: Boolean! + isFilterable: Boolean! + isNonNull: [KeystoneAdminUIFieldMetaIsNonNull!] + fieldMeta: JSON + viewsIndex: Int! + customViewsIndex: Int + createView: KeystoneAdminUIFieldMetaCreateView! + listView: KeystoneAdminUIFieldMetaListView! + itemView(id: ID): KeystoneAdminUIFieldMetaItemView + search: QueryMode +} + +enum KeystoneAdminUIFieldMetaIsNonNull { + read + create + update +} + +type KeystoneAdminUIFieldMetaCreateView { + fieldMode: KeystoneAdminUIFieldMetaCreateViewFieldMode! +} + +enum KeystoneAdminUIFieldMetaCreateViewFieldMode { + edit + hidden +} + +type KeystoneAdminUIFieldMetaListView { + fieldMode: KeystoneAdminUIFieldMetaListViewFieldMode! +} + +enum KeystoneAdminUIFieldMetaListViewFieldMode { + read + hidden +} + +type KeystoneAdminUIFieldMetaItemView { + fieldMode: KeystoneAdminUIFieldMetaItemViewFieldMode + fieldPosition: KeystoneAdminUIFieldMetaItemViewFieldPosition +} + +enum KeystoneAdminUIFieldMetaItemViewFieldMode { + edit + read + hidden +} + +enum KeystoneAdminUIFieldMetaItemViewFieldPosition { + form + sidebar +} + +enum QueryMode { + default + insensitive +} + +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + +type KeystoneAdminUISort { + field: String! + direction: KeystoneAdminUISortDirection! +} + +enum KeystoneAdminUISortDirection { + ASC + DESC +} diff --git a/examples/custom-session-next-auth/schema.prisma b/examples/custom-session-next-auth/schema.prisma new file mode 100644 index 00000000000..29655c77abf --- /dev/null +++ b/examples/custom-session-next-auth/schema.prisma @@ -0,0 +1,19 @@ +// This file is automatically generated by Keystone, do not modify it manually. +// Modify your Keystone config when you want to change this. + +datasource sqlite { + url = env("DATABASE_URL") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") + provider = "sqlite" +} + +generator client { + provider = "prisma-client-js" + output = "node_modules/.myprisma/client" +} + +model Post { + id String @id @default(cuid()) + title String @default("") + content String @default("") +} diff --git a/examples/custom-session-next-auth/schema.ts b/examples/custom-session-next-auth/schema.ts new file mode 100644 index 00000000000..2f964a6fef7 --- /dev/null +++ b/examples/custom-session-next-auth/schema.ts @@ -0,0 +1,27 @@ +import { list } from '@keystone-6/core'; +import { text } from '@keystone-6/core/fields'; +import type { Session } from 'next-auth'; +import type { Lists } from '.keystone/types'; + +// WARNING: this example is for demonstration purposes only +// as with each of our examples, it has not been vetted +// or tested for any particular usage + +function hasSession({ session }: { session?: Session }) { + return Boolean(session); +} + +export const lists: Lists = { + Post: list({ + // WARNING - for this example, anyone can create, query, update and delete anything + access: hasSession, + + fields: { + title: text({ validation: { isRequired: true } }), + + // the document field can be used for making rich editable content + // learn more at https://keystonejs.com/docs/guides/document-fields + content: text(), + }, + }), +}; From 8e8b9db86aa3cd254ba7cfaa4cf1e518aa307b58 Mon Sep 17 00:00:00 2001 From: Josh Calder Date: Fri, 19 May 2023 19:00:33 +1000 Subject: [PATCH 02/21] update some comments --- examples/custom-session-next-auth/keystone.ts | 5 +---- examples/custom-session-next-auth/schema.ts | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/custom-session-next-auth/keystone.ts b/examples/custom-session-next-auth/keystone.ts index 6a61e932e5b..782d4766523 100644 --- a/examples/custom-session-next-auth/keystone.ts +++ b/examples/custom-session-next-auth/keystone.ts @@ -24,10 +24,7 @@ const nextAuthSession = { | Session | undefined; }, - // we don't need these unless we want to support the functions - // context.sessionStrategy.start - // context.sessionStrategy.end - // + // we don't need these as next-auth handle start and end for us async start() {}, async end() {}, }; diff --git a/examples/custom-session-next-auth/schema.ts b/examples/custom-session-next-auth/schema.ts index 2f964a6fef7..744f37c1cac 100644 --- a/examples/custom-session-next-auth/schema.ts +++ b/examples/custom-session-next-auth/schema.ts @@ -13,7 +13,8 @@ function hasSession({ session }: { session?: Session }) { export const lists: Lists = { Post: list({ - // WARNING - for this example, anyone can create, query, update and delete anything + // WARNING - for this example, anyone can that can login can create, query, update and delete anything + // -- anyone with an account on the auth provider you are using can login access: hasSession, fields: { From 2af810637b574f994af633380fb9641e59433b77 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 31 May 2023 15:28:00 +1000 Subject: [PATCH 03/21] update pnpm-lock --- pnpm-lock.yaml | 108 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32046c1824a..7bdf7282d2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -881,6 +881,25 @@ importers: specifier: ~5.0.0 version: 5.0.2 + examples/custom-session-next-auth: + dependencies: + '@keystone-6/core': + specifier: ^5.0.0 + version: link:../../packages/core + '@prisma/client': + specifier: ^4.14.0 + version: 4.15.0(prisma@4.15.0) + next-auth: + specifier: ^4.22.1 + version: 4.22.1(next@13.3.0)(react-dom@18.2.0)(react@18.2.0) + devDependencies: + prisma: + specifier: ^4.14.0 + version: 4.15.0 + typescript: + specifier: ~5.0.0 + version: 5.0.2 + examples/custom-session-redis: dependencies: '@keystone-6/auth': @@ -8188,6 +8207,10 @@ packages: resolution: {integrity: sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==} engines: {node: '>=8.0.0'} + /@panva/hkdf@1.1.1: + resolution: {integrity: sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==} + dev: false + /@peculiar/asn1-schema@2.3.6: resolution: {integrity: sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==} dependencies: @@ -9059,7 +9082,7 @@ packages: engines: {node: '>=14'} dependencies: '@babel/code-frame': 7.21.4 - '@babel/runtime': 7.16.3 + '@babel/runtime': 7.21.5 '@types/aria-query': 5.0.1 aria-query: 5.1.3 chalk: 4.1.2 @@ -9136,7 +9159,7 @@ packages: peerDependencies: graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || 14 || 15 || 16 dependencies: - '@babel/runtime': 7.16.3 + '@babel/runtime': 7.21.5 find-pkg-json-field-up: 1.0.1 graphql: 16.6.0 lazy-require.macro: 0.1.0 @@ -10602,7 +10625,7 @@ packages: /babel-plugin-macros@2.8.0: resolution: {integrity: sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==} dependencies: - '@babel/runtime': 7.16.3 + '@babel/runtime': 7.21.5 cosmiconfig: 6.0.0 resolve: 1.20.0 dev: false @@ -10611,7 +10634,7 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} dependencies: - '@babel/runtime': 7.16.3 + '@babel/runtime': 7.21.5 cosmiconfig: 7.1.0 resolve: 1.20.0 dev: false @@ -12265,7 +12288,7 @@ packages: /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: - '@babel/runtime': 7.16.3 + '@babel/runtime': 7.21.5 csstype: 3.1.2 dev: false @@ -15926,6 +15949,10 @@ packages: '@sideway/pinpoint': 2.0.0 dev: true + /jose@4.14.4: + resolution: {integrity: sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==} + dev: false + /jpeg-js@0.4.4: resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} @@ -16750,7 +16777,7 @@ packages: /media-query-parser@2.0.2: resolution: {integrity: sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==} dependencies: - '@babel/runtime': 7.16.3 + '@babel/runtime': 7.21.5 dev: true /media-typer@0.3.0: @@ -17520,6 +17547,31 @@ packages: resolution: {integrity: sha512-md4cGoxuT4T4d/HDOXbrUHkTKrp/vp+m3aOA7XXVYwNsUNMK49g3SQicTSeV5GIz/5QVGAeYRAOlyp9OvlgsYA==} engines: {node: '>=10'} + /next-auth@4.22.1(next@13.3.0)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-NTR3f6W7/AWXKw8GSsgSyQcDW6jkslZLH8AiZa5PQ09w1kR8uHtR9rez/E9gAq/o17+p0JYHE8QjF3RoniiObA==} + peerDependencies: + next: ^12.2.5 || ^13 + nodemailer: ^6.6.5 + react: ^17.0.2 || ^18 + react-dom: ^17.0.2 || ^18 + peerDependenciesMeta: + nodemailer: + optional: true + dependencies: + '@babel/runtime': 7.21.5 + '@panva/hkdf': 1.1.1 + cookie: 0.5.0 + jose: 4.14.4 + next: 13.3.0(@babel/core@7.21.0)(react-dom@18.2.0)(react@18.2.0) + oauth: 0.9.15 + openid-client: 5.4.2 + preact: 10.15.1 + preact-render-to-string: 5.2.6(preact@10.15.1) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + uuid: 8.3.2 + dev: false + /next-compose-plugins@2.2.1: resolution: {integrity: sha512-OjJ+fV15FXO2uQXQagLD4C0abYErBjyjE0I0FHpOEIB8upw0hg1ldFP6cqHTJBH1cZqy96OeR3u1dJ+Ez2D4Bg==} dev: false @@ -17768,6 +17820,10 @@ packages: resolution: {integrity: sha512-6xpotnECFy/og7tKSBVmUNft7J3jyXAka4XvG6AUhFWRz+Q/Ljus7znJAA3bxColfQLdS+XsjoodtJfCgeTEFQ==} dev: true + /oauth@0.9.15: + resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -17781,6 +17837,11 @@ packages: kind-of: 3.2.2 dev: true + /object-hash@2.2.0: + resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} + engines: {node: '>= 6'} + dev: false + /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} @@ -17861,6 +17922,11 @@ packages: es-abstract: 1.21.2 dev: true + /oidc-token-hash@5.0.3: + resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==} + engines: {node: ^10.13.0 || >=12.0.0} + dev: false + /on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -17922,6 +17988,15 @@ packages: is-wsl: 2.2.0 dev: false + /openid-client@5.4.2: + resolution: {integrity: sha512-lIhsdPvJ2RneBm3nGBBhQchpe3Uka//xf7WPHTIglery8gnckvW7Bd9IaQzekzXJvWthCMyi/xVEyGW0RFPytw==} + dependencies: + jose: 4.14.4 + lru-cache: 6.0.0 + object-hash: 2.2.0 + oidc-token-hash: 5.0.3 + dev: false + /optimism@0.16.2: resolution: {integrity: sha512-zWNbgWj+3vLEjZNIh/okkY2EUfX+vB9TJopzIZwT1xxaMqC5hRLLraePod4c5n4He08xuXNH+zhKFFCu390wiQ==} dependencies: @@ -18603,6 +18678,19 @@ packages: xtend: 4.0.2 dev: false + /preact-render-to-string@5.2.6(preact@10.15.1): + resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} + peerDependencies: + preact: '>=10' + dependencies: + preact: 10.15.1 + pretty-format: 3.8.0 + dev: false + + /preact@10.15.1: + resolution: {integrity: sha512-qs2ansoQEwzNiV5eAcRT1p1EC/dmEzaATVDJNiB3g2sRDWdA7b7MurXdJjB2+/WQktGWZwxvDrnuRFbWuIr64g==} + dev: false + /preferred-pm@3.0.3: resolution: {integrity: sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==} engines: {node: '>=10'} @@ -18685,6 +18773,10 @@ packages: react-is: 18.2.0 dev: true + /pretty-format@3.8.0: + resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} + dev: false + /pretty-ms@7.0.1: resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} engines: {node: '>=10'} @@ -18944,7 +19036,7 @@ packages: peerDependencies: react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.16.3 + '@babel/runtime': 7.21.5 react: 18.2.0 dev: false @@ -19257,7 +19349,7 @@ packages: /regenerator-transform@0.15.1: resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==} dependencies: - '@babel/runtime': 7.16.3 + '@babel/runtime': 7.21.5 /regex-not@1.0.2: resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} From b72d9ec65b6e15c86e2aebf949d918549bcc420e Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 31 May 2023 15:32:08 +1000 Subject: [PATCH 04/21] add Session types --- examples/custom-session-next-auth/keystone.ts | 20 ++++++++++--------- examples/custom-session-next-auth/schema.ts | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/examples/custom-session-next-auth/keystone.ts b/examples/custom-session-next-auth/keystone.ts index 782d4766523..6e97d2cd0dd 100644 --- a/examples/custom-session-next-auth/keystone.ts +++ b/examples/custom-session-next-auth/keystone.ts @@ -13,23 +13,25 @@ import type { Context, TypeInfo } from '.keystone/types'; const nextAuthSession = { async get({ context }: { context: Context }): Promise { const { req, res } = context; - if (!req || !res || !req.headers.cookie) return; - // We need to construct the cookies object so next-auth gets to correct object + const { headers } = req ?? {}; + if (!headers?.cookie || !res) return; + + // next-auth needs an different cookies structure const cookies: Record = {}; - req.headers.cookie.split(';').forEach(cookie => { - const [key, value] = cookie.trim().split('='); + for (const part of headers.cookie.split(';')) { + const [key, value] = part.trim().split('='); cookies[key] = decodeURIComponent(value); - }); - return (await getServerSession({ headers: req.headers, cookies } as any, res, authOptions)) as - | Session - | undefined; + } + + return (await getServerSession({ headers, cookies } as any, res, authOptions)) ?? undefined; }, + // we don't need these as next-auth handle start and end for us async start() {}, async end() {}, }; -export default config({ +export default config>({ db: { provider: 'sqlite', url: process.env.DATABASE_URL || 'file:./keystone-example.db', diff --git a/examples/custom-session-next-auth/schema.ts b/examples/custom-session-next-auth/schema.ts index 744f37c1cac..2b05fc1951c 100644 --- a/examples/custom-session-next-auth/schema.ts +++ b/examples/custom-session-next-auth/schema.ts @@ -11,7 +11,7 @@ function hasSession({ session }: { session?: Session }) { return Boolean(session); } -export const lists: Lists = { +export const lists: Lists = { Post: list({ // WARNING - for this example, anyone can that can login can create, query, update and delete anything // -- anyone with an account on the auth provider you are using can login From cc0db79717b85f6c4622aa21011c47775b5b0ecc Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 31 May 2023 15:34:46 +1000 Subject: [PATCH 05/21] use same sessionSecret placeholder as other examples --- .../admin/pages/api/auth/[...nextauth].ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts b/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts index 301384723a6..eb3b188f44f 100644 --- a/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts +++ b/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts @@ -1,16 +1,21 @@ import NextAuth from 'next-auth'; import GithubProvider from 'next-auth/providers/github'; -// See https://next-auth.js.org/configuration/options for more details on what goes in here +// WARNING: this example is for demonstration purposes only +// as with each of our examples, it has not been vetted +// or tested for any particular usage + +// WARNING: you need to change this +const sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --'; + +// see https://next-auth.js.org/configuration/options for more export const authOptions = { - secret: '--DEV--COOKIE--SECRET--CHANGE--ME--==', - // Configure one or more authentication providers + secret: sessionSecret, providers: [ GithubProvider({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET!, }), - // ...add more providers here ], }; From 447ea15943291704c99bd8065ffae60352c3f893 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 31 May 2023 15:37:39 +1000 Subject: [PATCH 06/21] update comments --- examples/custom-session-next-auth/keystone.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/custom-session-next-auth/keystone.ts b/examples/custom-session-next-auth/keystone.ts index 6e97d2cd0dd..81f8948ccf8 100644 --- a/examples/custom-session-next-auth/keystone.ts +++ b/examples/custom-session-next-auth/keystone.ts @@ -16,7 +16,7 @@ const nextAuthSession = { const { headers } = req ?? {}; if (!headers?.cookie || !res) return; - // next-auth needs an different cookies structure + // next-auth needs a different cookies structure const cookies: Record = {}; for (const part of headers.cookie.split(';')) { const [key, value] = part.trim().split('='); @@ -40,7 +40,7 @@ export default config>({ ...fixPrismaPath, }, ui: { - // The following API routes are required for NextAuth.js + // the following api routes are required for nextauth.js publicPages: [ '/api/auth/csrf', '/api/auth/signin', @@ -49,11 +49,13 @@ export default config>({ '/api/auth/providers', '/api/auth/signout', '/api/auth/error', - // Each provider will need a separate callback and signin page listed here + + // each provider will need a separate callback and signin page listed here '/api/auth/signin/github', '/api/auth/callback/github', ], - // Adding Page middleware ensures that users are redirected to the signin page if they are not signed in. + + // adding page middleware ensures that users are redirected to the signin page if they are not signed in. pageMiddleware: async ({ wasAccessAllowed }) => { if (wasAccessAllowed) return; return { From 51403f10f9fb0b1ef4a188563e4a6f137f3c7f29 Mon Sep 17 00:00:00 2001 From: Josh Calder Date: Mon, 5 Jun 2023 16:21:50 +1000 Subject: [PATCH 07/21] Add user email and id to session --- .../admin/pages/api/auth/[...nextauth].ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts b/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts index eb3b188f44f..692a79bdc0a 100644 --- a/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts +++ b/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts @@ -11,6 +11,13 @@ const sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --'; // see https://next-auth.js.org/configuration/options for more export const authOptions = { secret: sessionSecret, + callbacks: { + async session({ session, token }) { + console.log('Next Auth Session Details', { session, token }); + // add the users subjectId and email to the session object + return { ...session, email: token.email, subjectId: token.sub }; + }, + }, providers: [ GithubProvider({ clientId: process.env.GITHUB_ID!, From 99014835d9b0c0b69f6fa719478213816b5bd9e8 Mon Sep 17 00:00:00 2001 From: Josh Calder Date: Mon, 5 Jun 2023 17:50:32 +1000 Subject: [PATCH 08/21] Add user to DB on signin --- .../admin/pages/api/auth/[...nextauth].ts | 25 +++++- examples/custom-session-next-auth/context.ts | 10 +++ examples/custom-session-next-auth/keystone.ts | 10 ++- .../custom-session-next-auth/schema.graphql | 88 +++++++++++++++++++ .../custom-session-next-auth/schema.prisma | 17 +++- examples/custom-session-next-auth/schema.ts | 17 +++- 6 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 examples/custom-session-next-auth/context.ts diff --git a/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts b/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts index 692a79bdc0a..9ca2553965c 100644 --- a/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts +++ b/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts @@ -1,6 +1,5 @@ import NextAuth from 'next-auth'; import GithubProvider from 'next-auth/providers/github'; - // WARNING: this example is for demonstration purposes only // as with each of our examples, it has not been vetted // or tested for any particular usage @@ -12,8 +11,28 @@ const sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --'; export const authOptions = { secret: sessionSecret, callbacks: { - async session({ session, token }) { - console.log('Next Auth Session Details', { session, token }); + async signIn({ user, account, profile }: any) { + // We need to require the context here to avoid a circular dependency + const sudoContext = require('../../../../context').sudo(); + // console.log('Next Auth Sign In Details', { user, account, profile }); + // check if the user exists in keystone + const keystoneUser = await sudoContext.query.User.findOne({ + where: { subjectId: profile.id }, + }); + // if not, create them + if (!keystoneUser) { + await sudoContext.query.User.createOne({ + data: { + subjectId: profile.id, + name: profile.name, + }, + }); + } + // return true to allow the sign in to complete + return true; + }, + async session({ session, token }: any) { + // console.log('Next Auth Session Details', { session, token }); // add the users subjectId and email to the session object return { ...session, email: token.email, subjectId: token.sub }; }, diff --git a/examples/custom-session-next-auth/context.ts b/examples/custom-session-next-auth/context.ts new file mode 100644 index 00000000000..2d55aeb4fc6 --- /dev/null +++ b/examples/custom-session-next-auth/context.ts @@ -0,0 +1,10 @@ +import { getContext } from '@keystone-6/core/context'; +import config from './keystone'; +// note this will probably be @prisma/client in your project +import * as PrismaModule from '.myprisma/client'; +import type { Context } from '.keystone/types'; + +export const keystoneContext: Context = + (globalThis as any).keystoneContext || getContext(config, PrismaModule); + +if (process.env.NODE_ENV !== 'production') (globalThis as any).keystoneContext = keystoneContext; diff --git a/examples/custom-session-next-auth/keystone.ts b/examples/custom-session-next-auth/keystone.ts index 81f8948ccf8..4746981189f 100644 --- a/examples/custom-session-next-auth/keystone.ts +++ b/examples/custom-session-next-auth/keystone.ts @@ -22,8 +22,14 @@ const nextAuthSession = { const [key, value] = part.trim().split('='); cookies[key] = decodeURIComponent(value); } - - return (await getServerSession({ headers, cookies } as any, res, authOptions)) ?? undefined; + // get the next-auth session + const nextAuthSession = await getServerSession({ headers, cookies } as any, res, authOptions); + // get the keystone user using the subjectId + const keystoneAuthor = await context.db.Author.findOne({ + where: { subjectId: nextAuthSession?.subjectId }, + }); + if (!keystoneAuthor) return; + return { ...nextAuthSession, itemId: keystoneAuthor.id }; }, // we don't need these as next-auth handle start and end for us diff --git a/examples/custom-session-next-auth/schema.graphql b/examples/custom-session-next-auth/schema.graphql index 9975f729501..0ee4ef78f9b 100644 --- a/examples/custom-session-next-auth/schema.graphql +++ b/examples/custom-session-next-auth/schema.graphql @@ -5,6 +5,7 @@ type Post { id: ID! title: String content: String + author: Author } input PostWhereUniqueInput { @@ -18,6 +19,7 @@ input PostWhereInput { id: IDFilter title: StringFilter content: StringFilter + author: AuthorWhereInput } input IDFilter { @@ -73,6 +75,13 @@ enum OrderDirection { input PostUpdateInput { title: String content: String + author: AuthorRelateToOneForUpdateInput +} + +input AuthorRelateToOneForUpdateInput { + create: AuthorCreateInput + connect: AuthorWhereUniqueInput + disconnect: Boolean } input PostUpdateArgs { @@ -83,6 +92,76 @@ input PostUpdateArgs { input PostCreateInput { title: String content: String + author: AuthorRelateToOneForCreateInput +} + +input AuthorRelateToOneForCreateInput { + create: AuthorCreateInput + connect: AuthorWhereUniqueInput +} + +type Author { + id: ID! + subjectId: String + name: String + posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] + postsCount(where: PostWhereInput! = {}): Int +} + +input AuthorWhereUniqueInput { + id: ID + subjectId: String +} + +input AuthorWhereInput { + AND: [AuthorWhereInput!] + OR: [AuthorWhereInput!] + NOT: [AuthorWhereInput!] + id: IDFilter + subjectId: StringFilter + name: StringFilter + posts: PostManyRelationFilter +} + +input PostManyRelationFilter { + every: PostWhereInput + some: PostWhereInput + none: PostWhereInput +} + +input AuthorOrderByInput { + id: OrderDirection + subjectId: OrderDirection + name: OrderDirection +} + +input AuthorUpdateInput { + subjectId: String + name: String + posts: PostRelateToManyForUpdateInput +} + +input PostRelateToManyForUpdateInput { + disconnect: [PostWhereUniqueInput!] + set: [PostWhereUniqueInput!] + create: [PostCreateInput!] + connect: [PostWhereUniqueInput!] +} + +input AuthorUpdateArgs { + where: AuthorWhereUniqueInput! + data: AuthorUpdateInput! +} + +input AuthorCreateInput { + subjectId: String + name: String + posts: PostRelateToManyForCreateInput +} + +input PostRelateToManyForCreateInput { + create: [PostCreateInput!] + connect: [PostWhereUniqueInput!] } """ @@ -97,6 +176,12 @@ type Mutation { updatePosts(data: [PostUpdateArgs!]!): [Post] deletePost(where: PostWhereUniqueInput!): Post deletePosts(where: [PostWhereUniqueInput!]!): [Post] + createAuthor(data: AuthorCreateInput!): Author + createAuthors(data: [AuthorCreateInput!]!): [Author] + updateAuthor(where: AuthorWhereUniqueInput!, data: AuthorUpdateInput!): Author + updateAuthors(data: [AuthorUpdateArgs!]!): [Author] + deleteAuthor(where: AuthorWhereUniqueInput!): Author + deleteAuthors(where: [AuthorWhereUniqueInput!]!): [Author] endSession: Boolean! } @@ -104,6 +189,9 @@ type Query { posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] post(where: PostWhereUniqueInput!): Post postsCount(where: PostWhereInput! = {}): Int + authors(where: AuthorWhereInput! = {}, orderBy: [AuthorOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: AuthorWhereUniqueInput): [Author!] + author(where: AuthorWhereUniqueInput!): Author + authorsCount(where: AuthorWhereInput! = {}): Int keystone: KeystoneMeta! } diff --git a/examples/custom-session-next-auth/schema.prisma b/examples/custom-session-next-auth/schema.prisma index 29655c77abf..968bcaadb83 100644 --- a/examples/custom-session-next-auth/schema.prisma +++ b/examples/custom-session-next-auth/schema.prisma @@ -13,7 +13,18 @@ generator client { } model Post { - id String @id @default(cuid()) - title String @default("") - content String @default("") + id String @id @default(cuid()) + title String @default("") + content String @default("") + author Author? @relation("Post_author", fields: [authorId], references: [id]) + authorId String? @map("author") + + @@index([authorId]) +} + +model Author { + id String @id @default(cuid()) + subjectId String @unique @default("") + name String @default("") + posts Post[] @relation("Post_author") } diff --git a/examples/custom-session-next-auth/schema.ts b/examples/custom-session-next-auth/schema.ts index 2b05fc1951c..fb86c083537 100644 --- a/examples/custom-session-next-auth/schema.ts +++ b/examples/custom-session-next-auth/schema.ts @@ -1,5 +1,6 @@ +import { denyAll, allOperations } from '@keystone-6/core/access'; import { list } from '@keystone-6/core'; -import { text } from '@keystone-6/core/fields'; +import { text, relationship } from '@keystone-6/core/fields'; import type { Session } from 'next-auth'; import type { Lists } from '.keystone/types'; @@ -23,6 +24,20 @@ export const lists: Lists = { // the document field can be used for making rich editable content // learn more at https://keystonejs.com/docs/guides/document-fields content: text(), + author: relationship({ ref: 'Author.posts', many: false }), + }, + }), + Author: list({ + access: { + operation: { + ...allOperations>(denyAll), + query: hasSession, + }, + }, + fields: { + subjectId: text({ isIndexed: 'unique' }), + name: text(), + posts: relationship({ ref: 'Post.author', many: true }), }, }), }; From acdd27cae27a773de8bbd16d6892bac527760f52 Mon Sep 17 00:00:00 2001 From: Josh Calder Date: Mon, 5 Jun 2023 18:04:58 +1000 Subject: [PATCH 09/21] Fix broken lock --- pnpm-lock.yaml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bdf7282d2b..3f16355add1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2832,7 +2832,7 @@ packages: '@babel/core': 7.21.0 '@babel/generator': 7.22.3 '@babel/parser': 7.22.4 - '@babel/runtime': 7.16.3 + '@babel/runtime': 7.22.3 '@babel/traverse': 7.22.4 '@babel/types': 7.22.4 babel-preset-fbjs: 3.4.0(@babel/core@7.21.0) @@ -9082,7 +9082,7 @@ packages: engines: {node: '>=14'} dependencies: '@babel/code-frame': 7.21.4 - '@babel/runtime': 7.21.5 + '@babel/runtime': 7.22.3 '@types/aria-query': 5.0.1 aria-query: 5.1.3 chalk: 4.1.2 @@ -9159,7 +9159,7 @@ packages: peerDependencies: graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || 14 || 15 || 16 dependencies: - '@babel/runtime': 7.21.5 + '@babel/runtime': 7.22.3 find-pkg-json-field-up: 1.0.1 graphql: 16.6.0 lazy-require.macro: 0.1.0 @@ -10625,7 +10625,7 @@ packages: /babel-plugin-macros@2.8.0: resolution: {integrity: sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==} dependencies: - '@babel/runtime': 7.21.5 + '@babel/runtime': 7.22.3 cosmiconfig: 6.0.0 resolve: 1.20.0 dev: false @@ -10634,7 +10634,7 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} dependencies: - '@babel/runtime': 7.21.5 + '@babel/runtime': 7.22.3 cosmiconfig: 7.1.0 resolve: 1.20.0 dev: false @@ -12288,7 +12288,7 @@ packages: /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: - '@babel/runtime': 7.21.5 + '@babel/runtime': 7.22.3 csstype: 3.1.2 dev: false @@ -16777,7 +16777,7 @@ packages: /media-query-parser@2.0.2: resolution: {integrity: sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==} dependencies: - '@babel/runtime': 7.21.5 + '@babel/runtime': 7.22.3 dev: true /media-typer@0.3.0: @@ -17558,7 +17558,7 @@ packages: nodemailer: optional: true dependencies: - '@babel/runtime': 7.21.5 + '@babel/runtime': 7.22.3 '@panva/hkdf': 1.1.1 cookie: 0.5.0 jose: 4.14.4 @@ -19036,7 +19036,7 @@ packages: peerDependencies: react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.21.5 + '@babel/runtime': 7.22.3 react: 18.2.0 dev: false @@ -19349,7 +19349,7 @@ packages: /regenerator-transform@0.15.1: resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==} dependencies: - '@babel/runtime': 7.21.5 + '@babel/runtime': 7.22.3 /regex-not@1.0.2: resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} @@ -19439,7 +19439,7 @@ packages: /relay-runtime@12.0.0: resolution: {integrity: sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==} dependencies: - '@babel/runtime': 7.16.3 + '@babel/runtime': 7.22.3 fbjs: 3.0.5 invariant: 2.2.4 transitivePeerDependencies: From dff7f0d8dbba108c3b677d08518f14b5c3ce97f0 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Thu, 8 Jun 2023 15:09:20 +1000 Subject: [PATCH 10/21] remove explicit type --- examples/custom-session-next-auth/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/custom-session-next-auth/schema.ts b/examples/custom-session-next-auth/schema.ts index fb86c083537..4062a16b057 100644 --- a/examples/custom-session-next-auth/schema.ts +++ b/examples/custom-session-next-auth/schema.ts @@ -30,7 +30,7 @@ export const lists: Lists = { Author: list({ access: { operation: { - ...allOperations>(denyAll), + ...allOperations(denyAll), query: hasSession, }, }, From e809033b5d99cf4e6850b186a0439e0f2fb8101a Mon Sep 17 00:00:00 2001 From: Josh Calder Date: Thu, 8 Jun 2023 15:27:34 +1000 Subject: [PATCH 11/21] Move around function --- .../admin/pages/api/auth/[...nextauth].ts | 28 +------- examples/custom-session-next-auth/context.ts | 10 --- examples/custom-session-next-auth/keystone.ts | 34 +--------- examples/custom-session-next-auth/schema.ts | 2 +- examples/custom-session-next-auth/session.ts | 66 +++++++++++++++++++ 5 files changed, 73 insertions(+), 67 deletions(-) delete mode 100644 examples/custom-session-next-auth/context.ts create mode 100644 examples/custom-session-next-auth/session.ts diff --git a/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts b/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts index 9ca2553965c..b359351a1fc 100644 --- a/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts +++ b/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts @@ -1,5 +1,6 @@ import NextAuth from 'next-auth'; import GithubProvider from 'next-auth/providers/github'; +import { getNextSession, onNextSignIn } from '../../../../session'; // WARNING: this example is for demonstration purposes only // as with each of our examples, it has not been vetted // or tested for any particular usage @@ -11,31 +12,8 @@ const sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --'; export const authOptions = { secret: sessionSecret, callbacks: { - async signIn({ user, account, profile }: any) { - // We need to require the context here to avoid a circular dependency - const sudoContext = require('../../../../context').sudo(); - // console.log('Next Auth Sign In Details', { user, account, profile }); - // check if the user exists in keystone - const keystoneUser = await sudoContext.query.User.findOne({ - where: { subjectId: profile.id }, - }); - // if not, create them - if (!keystoneUser) { - await sudoContext.query.User.createOne({ - data: { - subjectId: profile.id, - name: profile.name, - }, - }); - } - // return true to allow the sign in to complete - return true; - }, - async session({ session, token }: any) { - // console.log('Next Auth Session Details', { session, token }); - // add the users subjectId and email to the session object - return { ...session, email: token.email, subjectId: token.sub }; - }, + signIn: onNextSignIn, + session: getNextSession, }, providers: [ GithubProvider({ diff --git a/examples/custom-session-next-auth/context.ts b/examples/custom-session-next-auth/context.ts deleted file mode 100644 index 2d55aeb4fc6..00000000000 --- a/examples/custom-session-next-auth/context.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { getContext } from '@keystone-6/core/context'; -import config from './keystone'; -// note this will probably be @prisma/client in your project -import * as PrismaModule from '.myprisma/client'; -import type { Context } from '.keystone/types'; - -export const keystoneContext: Context = - (globalThis as any).keystoneContext || getContext(config, PrismaModule); - -if (process.env.NODE_ENV !== 'production') (globalThis as any).keystoneContext = keystoneContext; diff --git a/examples/custom-session-next-auth/keystone.ts b/examples/custom-session-next-auth/keystone.ts index 4746981189f..dbee658ebc9 100644 --- a/examples/custom-session-next-auth/keystone.ts +++ b/examples/custom-session-next-auth/keystone.ts @@ -1,42 +1,14 @@ import { config } from '@keystone-6/core'; -import { getServerSession } from 'next-auth/next'; -import type { Session } from 'next-auth'; import { fixPrismaPath } from '../example-utils'; import { lists } from './schema'; -import { authOptions } from './admin/pages/api/auth/[...nextauth]'; -import type { Context, TypeInfo } from '.keystone/types'; + +import { Session, nextAuthSession } from './session'; +import type { TypeInfo } from '.keystone/types'; // WARNING: this example is for demonstration purposes only // as with each of our examples, it has not been vetted // or tested for any particular usage -const nextAuthSession = { - async get({ context }: { context: Context }): Promise { - const { req, res } = context; - const { headers } = req ?? {}; - if (!headers?.cookie || !res) return; - - // next-auth needs a different cookies structure - const cookies: Record = {}; - for (const part of headers.cookie.split(';')) { - const [key, value] = part.trim().split('='); - cookies[key] = decodeURIComponent(value); - } - // get the next-auth session - const nextAuthSession = await getServerSession({ headers, cookies } as any, res, authOptions); - // get the keystone user using the subjectId - const keystoneAuthor = await context.db.Author.findOne({ - where: { subjectId: nextAuthSession?.subjectId }, - }); - if (!keystoneAuthor) return; - return { ...nextAuthSession, itemId: keystoneAuthor.id }; - }, - - // we don't need these as next-auth handle start and end for us - async start() {}, - async end() {}, -}; - export default config>({ db: { provider: 'sqlite', diff --git a/examples/custom-session-next-auth/schema.ts b/examples/custom-session-next-auth/schema.ts index 4062a16b057..4d55a00cd2c 100644 --- a/examples/custom-session-next-auth/schema.ts +++ b/examples/custom-session-next-auth/schema.ts @@ -1,7 +1,7 @@ import { denyAll, allOperations } from '@keystone-6/core/access'; import { list } from '@keystone-6/core'; import { text, relationship } from '@keystone-6/core/fields'; -import type { Session } from 'next-auth'; +import type { Session } from './session'; import type { Lists } from '.keystone/types'; // WARNING: this example is for demonstration purposes only diff --git a/examples/custom-session-next-auth/session.ts b/examples/custom-session-next-auth/session.ts new file mode 100644 index 00000000000..6f164a9122c --- /dev/null +++ b/examples/custom-session-next-auth/session.ts @@ -0,0 +1,66 @@ +import { getContext } from '@keystone-6/core/context'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from './admin/pages/api/auth/[...nextauth]'; +import config from './keystone'; +import * as PrismaModule from '.myprisma/client'; +import type { Context } from '.keystone/types'; + +export type Session = { + id: string; +}; +const sudoContext: Context = + (globalThis as any).keystoneContext || getContext(config, PrismaModule).sudo(); + +if (process.env.NODE_ENV !== 'production') (globalThis as any).sudoContext = sudoContext; + +export async function onNextSignIn({ user, account, profile }: any) { + // console.log('Next Auth Sign In Details', { user, account, profile }); + // check if the user exists in keystone + const keystoneUser = await sudoContext.query.Author.findOne({ + where: { subjectId: profile.id }, + }); + // if not, create them + if (!keystoneUser) { + await sudoContext.query.Author.createOne({ + data: { + subjectId: profile.id, + name: profile.name, + }, + }); + } + // return true to allow the sign in to complete + return true; +} + +export async function getNextSession({ session, token }: any) { + // console.log('Next Auth Session Details', { session, token }); + // add the users subjectId and email to the session object + return { ...session, email: token.email, subjectId: token.sub }; +} + +export const nextAuthSession = { + async get({ context }: { context: Context }): Promise { + const { req, res } = context; + const { headers } = req ?? {}; + if (!headers?.cookie || !res) return; + + // next-auth needs a different cookies structure + const cookies: Record = {}; + for (const part of headers.cookie.split(';')) { + const [key, value] = part.trim().split('='); + cookies[key] = decodeURIComponent(value); + } + // get the next-auth session + const nextAuthSession = await getServerSession({ headers, cookies } as any, res, authOptions); + // get the keystone user using the subjectId + const keystoneAuthor = await context.db.Author.findOne({ + where: { subjectId: nextAuthSession?.subjectId }, + }); + if (!keystoneAuthor) return; + return { id: keystoneAuthor.id }; + }, + + // we don't need these as next-auth handle start and end for us + async start() {}, + async end() {}, +}; From e46ef130a9c4207b13a64aae52fadf2585dc87eb Mon Sep 17 00:00:00 2001 From: Josh Calder Date: Thu, 8 Jun 2023 15:33:52 +1000 Subject: [PATCH 12/21] Fix context --- examples/custom-session-next-auth/session.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/custom-session-next-auth/session.ts b/examples/custom-session-next-auth/session.ts index 6f164a9122c..ae79b4128f2 100644 --- a/examples/custom-session-next-auth/session.ts +++ b/examples/custom-session-next-auth/session.ts @@ -8,12 +8,19 @@ import type { Context } from '.keystone/types'; export type Session = { id: string; }; -const sudoContext: Context = - (globalThis as any).keystoneContext || getContext(config, PrismaModule).sudo(); - -if (process.env.NODE_ENV !== 'production') (globalThis as any).sudoContext = sudoContext; +let _keystoneContext: Context = (globalThis as any)._keystoneContext; +function getKeystoneContext() { + if (!_keystoneContext) { + _keystoneContext = getContext(config, PrismaModule); + if (process.env.NODE_ENV !== 'production') { + (globalThis as any)._keystoneContext = _keystoneContext; + } + } + return _keystoneContext; +} export async function onNextSignIn({ user, account, profile }: any) { + const sudoContext = getKeystoneContext().sudo(); // console.log('Next Auth Sign In Details', { user, account, profile }); // check if the user exists in keystone const keystoneUser = await sudoContext.query.Author.findOne({ From 191bca88fc63adc1c913f77ad2a079b9f2dbe61c Mon Sep 17 00:00:00 2001 From: Josh Calder Date: Thu, 8 Jun 2023 15:49:27 +1000 Subject: [PATCH 13/21] move all to session.ts --- .../admin/pages/api/auth/[...nextauth].ts | 26 +----- examples/custom-session-next-auth/keystone.ts | 4 +- examples/custom-session-next-auth/session.ts | 80 ++++++++++++------- 3 files changed, 56 insertions(+), 54 deletions(-) diff --git a/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts b/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts index b359351a1fc..ba253e27930 100644 --- a/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts +++ b/examples/custom-session-next-auth/admin/pages/api/auth/[...nextauth].ts @@ -1,26 +1,4 @@ import NextAuth from 'next-auth'; -import GithubProvider from 'next-auth/providers/github'; -import { getNextSession, onNextSignIn } from '../../../../session'; -// WARNING: this example is for demonstration purposes only -// as with each of our examples, it has not been vetted -// or tested for any particular usage +import { nextAuthOptions } from '../../../../session'; -// WARNING: you need to change this -const sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --'; - -// see https://next-auth.js.org/configuration/options for more -export const authOptions = { - secret: sessionSecret, - callbacks: { - signIn: onNextSignIn, - session: getNextSession, - }, - providers: [ - GithubProvider({ - clientId: process.env.GITHUB_ID!, - clientSecret: process.env.GITHUB_SECRET!, - }), - ], -}; - -export default NextAuth(authOptions); +export default NextAuth(nextAuthOptions); diff --git a/examples/custom-session-next-auth/keystone.ts b/examples/custom-session-next-auth/keystone.ts index dbee658ebc9..acf5915ea10 100644 --- a/examples/custom-session-next-auth/keystone.ts +++ b/examples/custom-session-next-auth/keystone.ts @@ -2,7 +2,7 @@ import { config } from '@keystone-6/core'; import { fixPrismaPath } from '../example-utils'; import { lists } from './schema'; -import { Session, nextAuthSession } from './session'; +import { Session, nextAuthSessionStrategy } from './session'; import type { TypeInfo } from '.keystone/types'; // WARNING: this example is for demonstration purposes only @@ -44,5 +44,5 @@ export default config>({ }, lists, // you can find out more at https://keystonejs.com/docs/apis/session#session-api - session: nextAuthSession, + session: nextAuthSessionStrategy, }); diff --git a/examples/custom-session-next-auth/session.ts b/examples/custom-session-next-auth/session.ts index ae79b4128f2..28ac9f5d81a 100644 --- a/examples/custom-session-next-auth/session.ts +++ b/examples/custom-session-next-auth/session.ts @@ -1,13 +1,57 @@ import { getContext } from '@keystone-6/core/context'; import { getServerSession } from 'next-auth/next'; -import { authOptions } from './admin/pages/api/auth/[...nextauth]'; +import GithubProvider from 'next-auth/providers/github'; import config from './keystone'; import * as PrismaModule from '.myprisma/client'; import type { Context } from '.keystone/types'; +// WARNING: this example is for demonstration purposes only +// as with each of our examples, it has not been vetted +// or tested for any particular usage + +// WARNING: you need to change this +const sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --'; + +// see https://next-auth.js.org/configuration/options for more +export const nextAuthOptions = { + secret: sessionSecret, + callbacks: { + async signIn({ user, account, profile }: any) { + const sudoContext = getKeystoneContext().sudo(); + // console.log('Next Auth Sign In Details', { user, account, profile }); + // check if the user exists in keystone + const keystoneUser = await sudoContext.query.Author.findOne({ + where: { subjectId: profile.id }, + }); + // if not, create them + if (!keystoneUser) { + await sudoContext.query.Author.createOne({ + data: { + subjectId: profile.id, + name: profile.name, + }, + }); + } + // return true to allow the sign in to complete + return true; + }, + async session({ token, session }: any) { + // console.log('Next Auth Session Details', { session, token }); + // add the users subjectId and email to the session object + return { ...session, subjectId: token.sub }; + }, + }, + providers: [ + GithubProvider({ + clientId: process.env.GITHUB_ID!, + clientSecret: process.env.GITHUB_SECRET!, + }), + ], +}; export type Session = { id: string; }; + let _keystoneContext: Context = (globalThis as any)._keystoneContext; function getKeystoneContext() { @@ -19,33 +63,8 @@ function getKeystoneContext() { } return _keystoneContext; } -export async function onNextSignIn({ user, account, profile }: any) { - const sudoContext = getKeystoneContext().sudo(); - // console.log('Next Auth Sign In Details', { user, account, profile }); - // check if the user exists in keystone - const keystoneUser = await sudoContext.query.Author.findOne({ - where: { subjectId: profile.id }, - }); - // if not, create them - if (!keystoneUser) { - await sudoContext.query.Author.createOne({ - data: { - subjectId: profile.id, - name: profile.name, - }, - }); - } - // return true to allow the sign in to complete - return true; -} - -export async function getNextSession({ session, token }: any) { - // console.log('Next Auth Session Details', { session, token }); - // add the users subjectId and email to the session object - return { ...session, email: token.email, subjectId: token.sub }; -} -export const nextAuthSession = { +export const nextAuthSessionStrategy = { async get({ context }: { context: Context }): Promise { const { req, res } = context; const { headers } = req ?? {}; @@ -58,7 +77,12 @@ export const nextAuthSession = { cookies[key] = decodeURIComponent(value); } // get the next-auth session - const nextAuthSession = await getServerSession({ headers, cookies } as any, res, authOptions); + // TODO: get types for getServerSession. + const nextAuthSession = await getServerSession( + { headers, cookies } as any, + res, + nextAuthOptions + ); // get the keystone user using the subjectId const keystoneAuthor = await context.db.Author.findOne({ where: { subjectId: nextAuthSession?.subjectId }, From 023a31602509df1203b5c4b10cd0b4fc423cbae4 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 8 Jun 2023 17:55:56 +1000 Subject: [PATCH 14/21] fix circular import --- examples/custom-session-next-auth/session.ts | 29 ++++++++++---------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/examples/custom-session-next-auth/session.ts b/examples/custom-session-next-auth/session.ts index 28ac9f5d81a..c88ffc15a2f 100644 --- a/examples/custom-session-next-auth/session.ts +++ b/examples/custom-session-next-auth/session.ts @@ -2,8 +2,8 @@ import { getContext } from '@keystone-6/core/context'; import { getServerSession } from 'next-auth/next'; import GithubProvider from 'next-auth/providers/github'; import config from './keystone'; -import * as PrismaModule from '.myprisma/client'; import type { Context } from '.keystone/types'; + // WARNING: this example is for demonstration purposes only // as with each of our examples, it has not been vetted // or tested for any particular usage @@ -11,12 +11,25 @@ import type { Context } from '.keystone/types'; // WARNING: you need to change this const sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --'; +let _keystoneContext: Context = (globalThis as any)._keystoneContext; + +async function getKeystoneContext() { + if (_keystoneContext) return _keystoneContext; + + // TODO: this could probably be better + _keystoneContext = getContext(config, await import('.myprisma/client')); + if (process.env.NODE_ENV !== 'production') { + (globalThis as any)._keystoneContext = _keystoneContext; + } + return _keystoneContext; +} + // see https://next-auth.js.org/configuration/options for more export const nextAuthOptions = { secret: sessionSecret, callbacks: { async signIn({ user, account, profile }: any) { - const sudoContext = getKeystoneContext().sudo(); + const sudoContext = (await getKeystoneContext()).sudo(); // console.log('Next Auth Sign In Details', { user, account, profile }); // check if the user exists in keystone const keystoneUser = await sudoContext.query.Author.findOne({ @@ -52,18 +65,6 @@ export type Session = { id: string; }; -let _keystoneContext: Context = (globalThis as any)._keystoneContext; - -function getKeystoneContext() { - if (!_keystoneContext) { - _keystoneContext = getContext(config, PrismaModule); - if (process.env.NODE_ENV !== 'production') { - (globalThis as any)._keystoneContext = _keystoneContext; - } - } - return _keystoneContext; -} - export const nextAuthSessionStrategy = { async get({ context }: { context: Context }): Promise { const { req, res } = context; From 8e2c77b1fa6cdf81155666affe67659828178ac8 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 8 Jun 2023 18:03:12 +1000 Subject: [PATCH 15/21] add types --- examples/custom-session-next-auth/session.ts | 28 +++++++++++--------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/examples/custom-session-next-auth/session.ts b/examples/custom-session-next-auth/session.ts index c88ffc15a2f..fb79b510505 100644 --- a/examples/custom-session-next-auth/session.ts +++ b/examples/custom-session-next-auth/session.ts @@ -1,5 +1,6 @@ import { getContext } from '@keystone-6/core/context'; import { getServerSession } from 'next-auth/next'; +import type { CallbacksOptions } from 'next-auth'; import GithubProvider from 'next-auth/providers/github'; import config from './keystone'; import type { Context } from '.keystone/types'; @@ -28,15 +29,19 @@ async function getKeystoneContext() { export const nextAuthOptions = { secret: sessionSecret, callbacks: { - async signIn({ user, account, profile }: any) { + async signIn({ user, account, profile }: Parameters[0]) { + // console.error('next-auth signIn', { user, account, profile }); const sudoContext = (await getKeystoneContext()).sudo(); - // console.log('Next Auth Sign In Details', { user, account, profile }); + + if (!profile) return + // check if the user exists in keystone - const keystoneUser = await sudoContext.query.Author.findOne({ + const author = await sudoContext.query.Author.findOne({ where: { subjectId: profile.id }, }); - // if not, create them - if (!keystoneUser) { + + // if not, sign up + if (!author) { await sudoContext.query.Author.createOne({ data: { subjectId: profile.id, @@ -44,12 +49,11 @@ export const nextAuthOptions = { }, }); } - // return true to allow the sign in to complete - return true; + + return true; // accept the signin }, - async session({ token, session }: any) { - // console.log('Next Auth Session Details', { session, token }); - // add the users subjectId and email to the session object + async session({ token, session }: Parameters[0]) { + // console.error('next-auth session', { session, token }); return { ...session, subjectId: token.sub }; }, }, @@ -59,7 +63,7 @@ export const nextAuthOptions = { clientSecret: process.env.GITHUB_SECRET!, }), ], -}; +} ; export type Session = { id: string; @@ -77,8 +81,8 @@ export const nextAuthSessionStrategy = { const [key, value] = part.trim().split('='); cookies[key] = decodeURIComponent(value); } + // get the next-auth session - // TODO: get types for getServerSession. const nextAuthSession = await getServerSession( { headers, cookies } as any, res, From d8281a194f519df4375f6b4f669707d5e9b67cad Mon Sep 17 00:00:00 2001 From: Josh Calder Date: Thu, 8 Jun 2023 18:57:46 +1000 Subject: [PATCH 16/21] prettier --- examples/custom-session-next-auth/session.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/custom-session-next-auth/session.ts b/examples/custom-session-next-auth/session.ts index fb79b510505..2472ac31cbf 100644 --- a/examples/custom-session-next-auth/session.ts +++ b/examples/custom-session-next-auth/session.ts @@ -33,7 +33,7 @@ export const nextAuthOptions = { // console.error('next-auth signIn', { user, account, profile }); const sudoContext = (await getKeystoneContext()).sudo(); - if (!profile) return + if (!profile) return; // check if the user exists in keystone const author = await sudoContext.query.Author.findOne({ @@ -63,7 +63,7 @@ export const nextAuthOptions = { clientSecret: process.env.GITHUB_SECRET!, }), ], -} ; +}; export type Session = { id: string; From dd51b3227c832b6ebedb97d583c78a1d7e56e86f Mon Sep 17 00:00:00 2001 From: Josh Calder Date: Thu, 8 Jun 2023 20:08:56 +1000 Subject: [PATCH 17/21] await import config to avoid circular dependency --- examples/custom-session-next-auth/session.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/examples/custom-session-next-auth/session.ts b/examples/custom-session-next-auth/session.ts index 2472ac31cbf..deb597287ab 100644 --- a/examples/custom-session-next-auth/session.ts +++ b/examples/custom-session-next-auth/session.ts @@ -2,7 +2,6 @@ import { getContext } from '@keystone-6/core/context'; import { getServerSession } from 'next-auth/next'; import type { CallbacksOptions } from 'next-auth'; import GithubProvider from 'next-auth/providers/github'; -import config from './keystone'; import type { Context } from '.keystone/types'; // WARNING: this example is for demonstration purposes only @@ -18,7 +17,10 @@ async function getKeystoneContext() { if (_keystoneContext) return _keystoneContext; // TODO: this could probably be better - _keystoneContext = getContext(config, await import('.myprisma/client')); + _keystoneContext = getContext( + (await import('./keystone')).default, + await import('.myprisma/client') + ); if (process.env.NODE_ENV !== 'production') { (globalThis as any)._keystoneContext = _keystoneContext; } @@ -33,19 +35,17 @@ export const nextAuthOptions = { // console.error('next-auth signIn', { user, account, profile }); const sudoContext = (await getKeystoneContext()).sudo(); - if (!profile) return; - // check if the user exists in keystone const author = await sudoContext.query.Author.findOne({ - where: { subjectId: profile.id }, + where: { subjectId: user.id }, }); // if not, sign up if (!author) { await sudoContext.query.Author.createOne({ data: { - subjectId: profile.id, - name: profile.name, + subjectId: user.id, + name: user.name, }, }); } @@ -88,8 +88,10 @@ export const nextAuthSessionStrategy = { res, nextAuthOptions ); + if (!nextAuthSession) return; + const sudoContext = context.sudo(); // get the keystone user using the subjectId - const keystoneAuthor = await context.db.Author.findOne({ + const keystoneAuthor = await sudoContext.db.Author.findOne({ where: { subjectId: nextAuthSession?.subjectId }, }); if (!keystoneAuthor) return; From 629409b4838f537a7ad6aacf078037aa6a974c87 Mon Sep 17 00:00:00 2001 From: Josh Calder Date: Mon, 12 Jun 2023 16:15:57 +1000 Subject: [PATCH 18/21] add comment for prisma client import --- examples/custom-session-next-auth/session.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/custom-session-next-auth/session.ts b/examples/custom-session-next-auth/session.ts index deb597287ab..b6b1e7d468f 100644 --- a/examples/custom-session-next-auth/session.ts +++ b/examples/custom-session-next-auth/session.ts @@ -19,6 +19,9 @@ async function getKeystoneContext() { // TODO: this could probably be better _keystoneContext = getContext( (await import('./keystone')).default, + // We use the prisma client from the .myprisma folder in order to support multiple Prisma Clients in our examples + // your project will only have one Prisma Client, so you can use the following instead: + // await import('@primsa/client') await import('.myprisma/client') ); if (process.env.NODE_ENV !== 'production') { From 1805f312009db38feac62b427aa464d796e64930 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Tue, 13 Jun 2023 10:27:58 +1000 Subject: [PATCH 19/21] tidy up, and less complex types --- .../custom-session-next-auth/schema.graphql | 12 +++--- .../custom-session-next-auth/schema.prisma | 8 ++-- examples/custom-session-next-auth/schema.ts | 5 ++- examples/custom-session-next-auth/session.ts | 37 ++++++++++++------- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/examples/custom-session-next-auth/schema.graphql b/examples/custom-session-next-auth/schema.graphql index 0ee4ef78f9b..48ffae3a67b 100644 --- a/examples/custom-session-next-auth/schema.graphql +++ b/examples/custom-session-next-auth/schema.graphql @@ -102,7 +102,7 @@ input AuthorRelateToOneForCreateInput { type Author { id: ID! - subjectId: String + authId: String name: String posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] postsCount(where: PostWhereInput! = {}): Int @@ -110,7 +110,7 @@ type Author { input AuthorWhereUniqueInput { id: ID - subjectId: String + authId: String } input AuthorWhereInput { @@ -118,7 +118,7 @@ input AuthorWhereInput { OR: [AuthorWhereInput!] NOT: [AuthorWhereInput!] id: IDFilter - subjectId: StringFilter + authId: StringFilter name: StringFilter posts: PostManyRelationFilter } @@ -131,12 +131,12 @@ input PostManyRelationFilter { input AuthorOrderByInput { id: OrderDirection - subjectId: OrderDirection + authId: OrderDirection name: OrderDirection } input AuthorUpdateInput { - subjectId: String + authId: String name: String posts: PostRelateToManyForUpdateInput } @@ -154,7 +154,7 @@ input AuthorUpdateArgs { } input AuthorCreateInput { - subjectId: String + authId: String name: String posts: PostRelateToManyForCreateInput } diff --git a/examples/custom-session-next-auth/schema.prisma b/examples/custom-session-next-auth/schema.prisma index 968bcaadb83..9d951970e27 100644 --- a/examples/custom-session-next-auth/schema.prisma +++ b/examples/custom-session-next-auth/schema.prisma @@ -23,8 +23,8 @@ model Post { } model Author { - id String @id @default(cuid()) - subjectId String @unique @default("") - name String @default("") - posts Post[] @relation("Post_author") + id String @id @default(cuid()) + authId String @unique @default("") + name String @default("") + posts Post[] @relation("Post_author") } diff --git a/examples/custom-session-next-auth/schema.ts b/examples/custom-session-next-auth/schema.ts index 4d55a00cd2c..6fb86ebcbec 100644 --- a/examples/custom-session-next-auth/schema.ts +++ b/examples/custom-session-next-auth/schema.ts @@ -35,7 +35,10 @@ export const lists: Lists = { }, }, fields: { - subjectId: text({ isIndexed: 'unique' }), + // this is the authentication identifier provided by our next-auth session + // which we use to identify a user + authId: text({ isIndexed: 'unique' }), + name: text(), posts: relationship({ ref: 'Post.author', many: true }), }, diff --git a/examples/custom-session-next-auth/session.ts b/examples/custom-session-next-auth/session.ts index b6b1e7d468f..fa367475d32 100644 --- a/examples/custom-session-next-auth/session.ts +++ b/examples/custom-session-next-auth/session.ts @@ -1,6 +1,7 @@ import { getContext } from '@keystone-6/core/context'; import { getServerSession } from 'next-auth/next'; -import type { CallbacksOptions } from 'next-auth'; +import type { DefaultJWT } from 'next-auth/jwt'; +import type { DefaultUser } from 'next-auth'; import GithubProvider from 'next-auth/providers/github'; import type { Context } from '.keystone/types'; @@ -34,20 +35,20 @@ async function getKeystoneContext() { export const nextAuthOptions = { secret: sessionSecret, callbacks: { - async signIn({ user, account, profile }: Parameters[0]) { + async signIn({ user }: { user: DefaultUser }) { // console.error('next-auth signIn', { user, account, profile }); const sudoContext = (await getKeystoneContext()).sudo(); // check if the user exists in keystone const author = await sudoContext.query.Author.findOne({ - where: { subjectId: user.id }, + where: { authId: user.id }, }); // if not, sign up if (!author) { await sudoContext.query.Author.createOne({ data: { - subjectId: user.id, + authId: user.id, name: user.name, }, }); @@ -55,12 +56,21 @@ export const nextAuthOptions = { return true; // accept the signin }, - async session({ token, session }: Parameters[0]) { + + async session({ + token: { + sub: authId, // may be missing + }, + }: { + token: DefaultJWT; + }) { // console.error('next-auth session', { session, token }); - return { ...session, subjectId: token.sub }; + if (!authId) return; + return { authId }; }, }, providers: [ + // allow anyone with a GitHub account to sign up as an author GithubProvider({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET!, @@ -73,7 +83,7 @@ export type Session = { }; export const nextAuthSessionStrategy = { - async get({ context }: { context: Context }): Promise { + async get({ context }: { context: Context }) { const { req, res } = context; const { headers } = req ?? {}; if (!headers?.cookie || !res) return; @@ -85,20 +95,19 @@ export const nextAuthSessionStrategy = { cookies[key] = decodeURIComponent(value); } - // get the next-auth session const nextAuthSession = await getServerSession( { headers, cookies } as any, res, nextAuthOptions ); if (!nextAuthSession) return; - const sudoContext = context.sudo(); - // get the keystone user using the subjectId - const keystoneAuthor = await sudoContext.db.Author.findOne({ - where: { subjectId: nextAuthSession?.subjectId }, + + const author = await context.sudo().db.Author.findOne({ + where: { authId: nextAuthSession.authId }, }); - if (!keystoneAuthor) return; - return { id: keystoneAuthor.id }; + if (!author) return; + + return { id: author.id }; }, // we don't need these as next-auth handle start and end for us From e314b7cf368bb0e3f91690941abbc5b10592df94 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Tue, 13 Jun 2023 11:42:58 +1000 Subject: [PATCH 20/21] fix types for NextAuthOptions --- examples/custom-session-next-auth/session.ts | 24 ++++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/examples/custom-session-next-auth/session.ts b/examples/custom-session-next-auth/session.ts index fa367475d32..fa40ae18824 100644 --- a/examples/custom-session-next-auth/session.ts +++ b/examples/custom-session-next-auth/session.ts @@ -1,7 +1,7 @@ import { getContext } from '@keystone-6/core/context'; import { getServerSession } from 'next-auth/next'; import type { DefaultJWT } from 'next-auth/jwt'; -import type { DefaultUser } from 'next-auth'; +import type { DefaultSession, DefaultUser } from 'next-auth'; import GithubProvider from 'next-auth/providers/github'; import type { Context } from '.keystone/types'; @@ -57,16 +57,17 @@ export const nextAuthOptions = { return true; // accept the signin }, - async session({ - token: { - sub: authId, // may be missing - }, - }: { - token: DefaultJWT; + async session({ session, token }: { + session: DefaultSession, // required by next-auth, not by us + token: DefaultJWT }) { // console.error('next-auth session', { session, token }); - if (!authId) return; - return { authId }; + return { + ...session, + keystone: { + authId: token.sub, + } + }; }, }, providers: [ @@ -102,8 +103,11 @@ export const nextAuthSessionStrategy = { ); if (!nextAuthSession) return; + const { authId } = nextAuthSession.keystone; + if (!authId) return; + const author = await context.sudo().db.Author.findOne({ - where: { authId: nextAuthSession.authId }, + where: { authId }, }); if (!author) return; From 18bc115ec181ca5fafec1f1e267f768ec9b8b1a5 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Tue, 13 Jun 2023 11:46:30 +1000 Subject: [PATCH 21/21] prettier --- examples/custom-session-next-auth/session.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/custom-session-next-auth/session.ts b/examples/custom-session-next-auth/session.ts index fa40ae18824..2351fc13fca 100644 --- a/examples/custom-session-next-auth/session.ts +++ b/examples/custom-session-next-auth/session.ts @@ -57,16 +57,19 @@ export const nextAuthOptions = { return true; // accept the signin }, - async session({ session, token }: { - session: DefaultSession, // required by next-auth, not by us - token: DefaultJWT + async session({ + session, + token, + }: { + session: DefaultSession; // required by next-auth, not by us + token: DefaultJWT; }) { // console.error('next-auth session', { session, token }); return { ...session, keystone: { authId: token.sub, - } + }, }; }, },