Skip to content

Commit c9db964

Browse files
committed
Remove session end/start from core and provide generic fetch-style request and response on context instead of node-specific
1 parent 4e21a90 commit c9db964

File tree

51 files changed

+648
-538
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+648
-538
lines changed

examples/auth-magic-link/keystone.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
11
import { config } from '@keystone-6/core'
2-
import { statelessSessions } from '@keystone-6/core/session'
32
import { createAuth } from '@keystone-6/auth'
4-
import { lists, extendGraphqlSchema } from './schema'
3+
import { lists, extendGraphqlSchema, sessionStrategy } from './schema'
54
import type { TypeInfo } from '.keystone/types'
65

76
// WARNING: this example is for demonstration purposes only
87
// as with each of our examples, it has not been vetted
98
// or tested for any particular usage
109

11-
// WARNING: you need to change this
12-
const sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --'
13-
14-
// statelessSessions uses cookies for session tracking
15-
// these cookies have an expiry, in seconds
16-
// we use an expiry of one hour for this example
17-
const sessionMaxAge = 60 * 60
18-
1910
// withAuth is a function we can use to wrap our base configuration
2011
const { withAuth } = createAuth({
2112
// this is the list that contains our users
@@ -36,6 +27,11 @@ const { withAuth } = createAuth({
3627
// the following fields are used by the "Create First User" form
3728
fields: ['name', 'password'],
3829
},
30+
sessionStrategy,
31+
getSession: ({ context, data }) =>
32+
context.query.User.findOne({
33+
where: { id: data.itemId },
34+
}),
3935
})
4036

4137
export default withAuth<TypeInfo>(
@@ -51,12 +47,5 @@ export default withAuth<TypeInfo>(
5147
graphql: {
5248
extendGraphqlSchema,
5349
},
54-
// you can find out more at https://keystonejs.com/docs/apis/session#session-api
55-
session: statelessSessions({
56-
// the maxAge option controls how long session cookies are valid for before they expire
57-
maxAge: sessionMaxAge,
58-
// the session secret is used to encrypt cookie data
59-
secret: sessionSecret,
60-
}),
6150
})
6251
)

examples/auth-magic-link/schema.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,44 @@ import { password, text, timestamp } from '@keystone-6/core/fields'
44
import type { Lists, Context, Session } from '.keystone/types'
55

66
import { randomBytes } from 'node:crypto'
7+
import { statelessSessions } from '@keystone-6/auth'
78

89
const g = gWithContext<Context>()
910
type g<T> = gWithContext.infer<T>
1011

1112
declare module '.keystone/types' {
1213
interface Session {
13-
itemId: string
14-
listKey: string
14+
id: string
1515
}
1616
}
1717

1818
function hasSession({ session }: { session?: Session }) {
1919
return Boolean(session)
2020
}
2121

22+
// WARNING: you need to change this
23+
const sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --'
24+
25+
// statelessSessions uses cookies for session tracking
26+
// these cookies have an expiry, in seconds
27+
// we use an expiry of one hour for this example
28+
const sessionMaxAge = 60 * 60
29+
30+
export const sessionStrategy = statelessSessions({
31+
// the maxAge option controls how long session cookies are valid for before they expire
32+
maxAge: sessionMaxAge,
33+
// the session secret is used to encrypt cookie data
34+
secret: sessionSecret,
35+
})
36+
2237
function isSameUserFilter({ session }: { session?: Session }) {
2338
// you need to have a session to do this
2439
if (!session) return false
2540

2641
// only yourself
2742
return {
2843
id: {
29-
equals: session.itemId,
44+
equals: session.id,
3045
},
3146
}
3247
}
@@ -148,9 +163,6 @@ export const extendGraphqlSchema = g.extend(base => {
148163
},
149164

150165
async resolve(args, { userId, token }, context) {
151-
if (!context.sessionStrategy)
152-
throw new Error('No session implementation available on context')
153-
154166
const kdf = (base.schema.getType('User') as any).getFields()?.password.extensions
155167
?.keystoneSecretField
156168
const sudoContext = context.sudo()
@@ -176,10 +188,9 @@ export const extendGraphqlSchema = g.extend(base => {
176188
},
177189
})
178190

179-
await context.sessionStrategy.start({
191+
await sessionStrategy.start({
180192
context,
181193
data: {
182-
listKey: 'User',
183194
itemId: userId,
184195
},
185196
})

examples/auth/keystone.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { config } from '@keystone-6/core'
2-
import { statelessSessions } from '@keystone-6/core/session'
3-
import { createAuth } from '@keystone-6/auth'
2+
import { createAuth, statelessSessions } from '@keystone-6/auth'
43
import { lists } from './schema'
54
import type { TypeInfo } from '.keystone/types'
65

@@ -42,9 +41,19 @@ const { withAuth } = createAuth({
4241
isAdmin: true,
4342
},
4443
},
45-
46-
// add isAdmin to the session data
47-
sessionData: 'isAdmin',
44+
sessionStrategy: statelessSessions({
45+
// the maxAge option controls how long session cookies are valid for before they expire
46+
maxAge: sessionMaxAge,
47+
// the session secret is used to encrypt cookie data
48+
secret: sessionSecret,
49+
}),
50+
async getSession({ context, data }) {
51+
const user = await context.db.User.findOne({
52+
where: { id: data.itemId },
53+
})
54+
if (!user) return
55+
return { user }
56+
},
4857
})
4958

5059
export default withAuth<TypeInfo>(
@@ -60,15 +69,8 @@ export default withAuth<TypeInfo>(
6069
ui: {
6170
// only admins can view the AdminUI
6271
isAccessAllowed: context => {
63-
return context.session?.data?.isAdmin ?? false
72+
return context.session?.user.isAdmin ?? false
6473
},
6574
},
66-
// you can find out more at https://keystonejs.com/docs/apis/session#session-api
67-
session: statelessSessions({
68-
// the maxAge option controls how long session cookies are valid for before they expire
69-
maxAge: sessionMaxAge,
70-
// the session secret is used to encrypt cookie data
71-
secret: sessionSecret,
72-
}),
7375
})
7476
)

examples/auth/schema.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@ import type { Lists, Session } from '.keystone/types'
88
// or tested for any particular usage
99
declare module '.keystone/types' {
1010
interface Session {
11-
itemId: string
12-
data: {
13-
isAdmin: boolean
14-
}
11+
user: Lists.User.Item
1512
}
1613
}
1714

@@ -24,26 +21,26 @@ function isAdminOrSameUser({ session, item }: { session?: Session; item: Lists.U
2421
if (!session) return false
2522

2623
// admins can do anything
27-
if (session.data.isAdmin) return true
24+
if (session.user.isAdmin) return true
2825

2926
// no item? then no
3027
if (!item) return false
3128

3229
// the authenticated user needs to be equal to the user we are updating
33-
return session.itemId === item.id
30+
return session.user.id === item.id
3431
}
3532

3633
function isAdminOrSameUserFilter({ session }: { session?: Session }) {
3734
// you need to have a session to do this
3835
if (!session) return false
3936

4037
// admins can see everything
41-
if (session.data?.isAdmin) return {}
38+
if (session.user.isAdmin) return {}
4239

4340
// only yourself
4441
return {
4542
id: {
46-
equals: session.itemId,
43+
equals: session.user.id,
4744
},
4845
}
4946
}
@@ -53,7 +50,7 @@ function isAdmin({ session }: { session?: Session }) {
5350
if (!session) return false
5451

5552
// admins can do anything
56-
if (session.data.isAdmin) return true
53+
if (session.user.isAdmin) return true
5754

5855
// otherwise, no
5956
return false
Lines changed: 49 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { config } from '@keystone-6/core'
2-
import { statelessSessions } from '@keystone-6/core/session'
3-
import { createAuth } from '@keystone-6/auth'
2+
import { createAuth, SessionStrategy, statelessSessions } from '@keystone-6/auth'
43
import { lists } from './schema'
5-
import type { Config, Context, TypeInfo, Session } from '.keystone/types'
4+
import type { TypeInfo, Lists } from '.keystone/types'
5+
import type { BaseKeystoneTypeInfo } from '@keystone-6/core/types'
66

77
// WARNING: this example is for demonstration purposes only
88
// as with each of our examples, it has not been vetted
99
// or tested for any particular usage
1010

1111
// withAuth is a function we can use to wrap our base configuration
12-
const { withAuth } = createAuth({
12+
const { withAuth } = createAuth<Lists.User.TypeInfo, { itemId: string; startedAt: number }>({
1313
// this is the list that contains our users
1414
listKey: 'User',
1515

@@ -29,55 +29,57 @@ const { withAuth } = createAuth({
2929
fields: ['name', 'password'],
3030
},
3131

32-
sessionData: 'passwordChangedAt',
32+
sessionStrategy: withSessionStartedAt(statelessSessions()),
33+
async getSession({ context, data }) {
34+
const user = await context.db.User.findOne({
35+
where: { id: data.itemId },
36+
})
37+
if (!user) return
38+
if (user.passwordChangedAt && user.passwordChangedAt > new Date(data.startedAt)) {
39+
return
40+
}
41+
return { user }
42+
},
3343
})
3444

35-
function withSessionInvalidation(config: Config): Config {
36-
const existingSessionStrategy = config.session!
37-
45+
function withSessionStartedAt<T, TypeInfo extends BaseKeystoneTypeInfo>(
46+
existingSessionStrategy: SessionStrategy<
47+
T & { startedAt: number },
48+
T & { startedAt: number },
49+
TypeInfo
50+
>
51+
): SessionStrategy<T, T & { startedAt: number }, TypeInfo> {
3852
return {
39-
...config,
40-
session: {
41-
...existingSessionStrategy,
42-
async get({ context }: { context: Context }): Promise<Session | undefined> {
43-
const session = await existingSessionStrategy.get({ context })
44-
if (!session) return
45-
46-
// has the password changed since the session started?
47-
if (new Date(session.data.passwordChangedAt) > new Date(session.startedAt)) {
48-
// invalidate the session if password changed
49-
await existingSessionStrategy.end({ context })
50-
return
51-
}
52-
53-
return session
54-
},
55-
async start({ context, data }: { context: Context; data: Session }) {
56-
return await existingSessionStrategy.start({
57-
context,
58-
data: {
59-
...data,
60-
startedAt: Date.now(),
61-
},
62-
})
63-
},
53+
async start({ context, data }) {
54+
await existingSessionStrategy.start({
55+
context,
56+
data: { ...data, startedAt: Date.now() },
57+
})
58+
},
59+
async get({ context }) {
60+
const session = await existingSessionStrategy.get({ context })
61+
if (
62+
!session ||
63+
typeof session !== 'object' ||
64+
!('startedAt' in session) ||
65+
typeof session.startedAt !== 'number'
66+
)
67+
return
68+
return { ...session, startedAt: session.startedAt }
6469
},
70+
end: existingSessionStrategy.end,
6571
}
6672
}
6773

68-
export default withSessionInvalidation(
69-
withAuth(
70-
config<TypeInfo>({
71-
db: {
72-
provider: 'sqlite',
73-
url: process.env.DATABASE_URL || 'file:./keystone-example.db',
74+
export default withAuth(
75+
config<TypeInfo>({
76+
db: {
77+
provider: 'sqlite',
78+
url: process.env.DATABASE_URL || 'file:./keystone-example.db',
7479

75-
// WARNING: this is only needed for our monorepo examples, dont do this
76-
prismaClientPath: 'node_modules/myprisma',
77-
},
78-
lists,
79-
// you can find out more at https://keystonejs.com/docs/apis/session#session-api
80-
session: statelessSessions<Session>(),
81-
})
82-
)
80+
// WARNING: this is only needed for our monorepo examples, dont do this
81+
prismaClientPath: 'node_modules/myprisma',
82+
},
83+
lists,
84+
})
8385
)

examples/custom-session-invalidation/schema.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,7 @@ import type { Lists, Session } from '.keystone/types'
1010
// needs to be compatible with withAuth
1111
declare module '.keystone/types' {
1212
interface Session {
13-
listKey: string
14-
itemId: string
15-
data: {
16-
passwordChangedAt: string
17-
}
18-
startedAt: number
13+
user: Lists.User.Item
1914
}
2015
}
2116

@@ -30,7 +25,7 @@ function isSameUserFilter({ session }: { session?: Session }) {
3025
// the authenticated user can only see themselves
3126
return {
3227
id: {
33-
equals: session.itemId,
28+
equals: session.user.id,
3429
},
3530
}
3631
}

0 commit comments

Comments
 (0)