Skip to content

Commit 81bfaf9

Browse files
authored
feat: add destroy, save and reload to session instance (#77)
* feat: add `destroy`, `save` and `reload` to session instance * tests * somewhat simpler test * Update types/types.test-d.ts
1 parent 37dff9a commit 81bfaf9

File tree

6 files changed

+190
-47
lines changed

6 files changed

+190
-47
lines changed

README.md

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,11 @@ app.addHook('preHandler', (request, reply, next) => {
3636
})
3737
```
3838
**NOTE**: For all unencrypted (HTTP) connections, you need to set the `secure` cookie option to `false`. See below for all cookie options and their details.
39-
The `sessionStore` decorator of the `request` allows to get, save and delete sessions.
39+
The `session` object has methods that allow you to get, save, reload and delete sessions.
4040
```js
4141
app.register(fastifySession, {secret: 'a secret with minimum length of 32 characters'});
4242
app.addHook('preHandler', (request, reply, next) => {
43-
const session = request.session;
44-
request.sessionStore.destroy(session.sessionId, next);
43+
request.session.destroy(next);
4544
})
4645
```
4746

@@ -103,24 +102,37 @@ idGenerator: (request) => {
103102

104103
Allows to access or modify the session data.
105104

106-
#### request.destroySession(callback)
105+
#### Session#destroy(callback)
107106

108107
Allows to destroy the session in the store
109108

110109
#### Session#touch()
111110

112111
Updates the `expires` property of the session.
113112

114-
#### Session#regenerate()
113+
#### Session#regenerate(callback)
115114

116-
Regenerates the session by generating a new `sessionId`.
115+
Regenerates the session by generating a new `sessionId` and persist it to the store.
117116
```js
118-
fastify.get('/regenerate', (request, reply) => {
119-
request.session.regenerate();
120-
reply.send(request.session.sessionId);
117+
fastify.get('/regenerate', (request, reply, done) => {
118+
request.session.regenerate(error => {
119+
if (error) {
120+
done(error);
121+
return;
122+
}
123+
reply.send(request.session.sessionId);
124+
});
121125
});
122126
```
123127

128+
#### Session#reload(callback)
129+
130+
Reloads the session data from the store and re-populates the `request.session` object.
131+
132+
#### Session#save(callback)
133+
134+
Save the session back to the store, replacing the contents on the store with the contents in memory.
135+
124136
#### Session#get(key)
125137

126138
Gets a value from the session

lib/fastifySession.js

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ function session (fastify, options, next) {
2828
})
2929
fastify.decorateRequest('sessionStore', { getter: () => options.store })
3030
fastify.decorateRequest('session', null)
31-
fastify.decorateRequest('destroySession', destroySession)
3231
fastify.addHook('onRequest', onRequest(options))
3332
fastify.addHook('onSend', onSend(options))
3433
next()
@@ -65,7 +64,22 @@ function decryptSession (sessionId, options, request, done) {
6564
return
6665
}
6766
if (session && session.expires && session.expires <= Date.now()) {
68-
options.store.destroy(sessionId, getDestroyCallback(secret, request, done, cookieOpts, idGenerator))
67+
const restoredSession = Session.restore(
68+
request,
69+
idGenerator,
70+
cookieOpts,
71+
secret,
72+
session
73+
)
74+
75+
restoredSession.destroy(err => {
76+
if (err) {
77+
done(err)
78+
return
79+
}
80+
81+
restoredSession.regenerate(done)
82+
})
6983
return
7084
}
7185
if (options.rolling) {
@@ -118,7 +132,7 @@ function onSend (options) {
118132
done()
119133
return
120134
}
121-
options.store.set(session.sessionId, session, (err) => {
135+
session.save((err) => {
122136
if (err) {
123137
done(err)
124138
return
@@ -133,29 +147,11 @@ function onSend (options) {
133147
}
134148
}
135149

136-
function getDestroyCallback (secret, request, done, cookieOpts, idGenerator) {
137-
return function destroyCallback (err) {
138-
if (err) {
139-
done(err)
140-
return
141-
}
142-
newSession(secret, request, cookieOpts, idGenerator, done)
143-
}
144-
}
145-
146150
function newSession (secret, request, cookieOpts, idGenerator, done) {
147151
request.session = new Session(request, idGenerator, cookieOpts, secret)
148152
done()
149153
}
150154

151-
function destroySession (done) {
152-
const request = this
153-
request.sessionStore.destroy(request.session.sessionId, (err) => {
154-
request.session = null
155-
done(err)
156-
})
157-
}
158-
159155
function checkOptions (options) {
160156
if (!options.secret) {
161157
return new Error('the secret option is required!')

lib/session.js

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,22 @@ const sign = Symbol('sign')
99
const addDataToSession = Symbol('addDataToSession')
1010
const generateId = Symbol('generateId')
1111
const requestKey = Symbol('request')
12+
const cookieOptsKey = Symbol('cookieOpts')
1213

1314
module.exports = class Session {
1415
constructor (request, idGenerator, cookieOpts, secret, prevSession = {}) {
1516
this[generateId] = idGenerator
1617
this.expires = null
1718
this.cookie = new Cookie(cookieOpts)
19+
this[cookieOptsKey] = cookieOpts
1820
this[maxAge] = cookieOpts.maxAge
1921
this[secretKey] = secret
2022
this[addDataToSession](prevSession)
2123
this[requestKey] = request
2224
this.touch()
2325
if (!this.sessionId) {
24-
this.regenerate()
26+
this.sessionId = this[generateId](this[requestKey])
27+
this.encryptedSessionId = this[sign]()
2528
}
2629
}
2730

@@ -32,9 +35,14 @@ module.exports = class Session {
3235
}
3336
}
3437

35-
regenerate () {
36-
this.sessionId = this[generateId](this[requestKey])
37-
this.encryptedSessionId = this[sign]()
38+
regenerate (callback) {
39+
const session = new Session(this[requestKey], this[generateId], this[cookieOptsKey], this[secretKey])
40+
41+
this[requestKey].sessionStore.set(session.sessionId, session, error => {
42+
this[requestKey].session = session
43+
44+
callback(error)
45+
})
3846
}
3947

4048
[addDataToSession] (prevSession) {
@@ -53,6 +61,28 @@ module.exports = class Session {
5361
this[key] = value
5462
}
5563

64+
destroy (callback) {
65+
this[requestKey].sessionStore.destroy(this.sessionId, error => {
66+
this[requestKey].session = null
67+
68+
callback(error)
69+
})
70+
}
71+
72+
reload (callback) {
73+
this[requestKey].sessionStore.get(this.sessionId, (error, session) => {
74+
this[requestKey].session = new Session(this[requestKey], this[generateId], this[cookieOptsKey], this[secretKey], session)
75+
76+
callback(error)
77+
})
78+
}
79+
80+
save (callback) {
81+
this[requestKey].sessionStore.set(this.sessionId, this, error => {
82+
callback(error)
83+
})
84+
}
85+
5686
[sign] () {
5787
return cookieSignature.sign(this.sessionId, this[secretKey])
5888
}

test/session.test.js

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ test('should add session object to request', async (t) => {
2121
test('should destroy the session', async (t) => {
2222
t.plan(3)
2323
const port = await testServer((request, reply) => {
24-
request.destroySession((err) => {
24+
request.session.destroy((err) => {
2525
t.falsy(err)
2626
t.is(request.session, null)
2727
reply.send(200)
@@ -167,8 +167,13 @@ test('should generate new sessionId', async (t) => {
167167
fastify.register(fastifySession, options)
168168
fastify.get('/', (request, reply) => {
169169
oldSessionId = request.session.sessionId
170-
request.session.regenerate()
171-
reply.send(200)
170+
request.session.regenerate(error => {
171+
if (error) {
172+
reply.status(500).send('Error ' + error)
173+
} else {
174+
reply.send(200)
175+
}
176+
})
172177
})
173178
fastify.get('/check', (request, reply) => {
174179
t.not(request.session.sessionId, oldSessionId)
@@ -271,6 +276,40 @@ test('should decryptSession with custom cookie options', async (t) => {
271276
})
272277
})
273278

279+
test('should bubble up errors with destroy call if session expired', async (t) => {
280+
t.plan(2)
281+
const fastify = Fastify()
282+
const store = {
283+
set (id, data, cb) { cb(null) },
284+
get (id, cb) {
285+
cb(null, { expires: Date.now() - 1000, cookie: { expires: Date.now() - 1000 } })
286+
},
287+
destroy (id, cb) { cb(new Error('No can do')) }
288+
}
289+
290+
const options = {
291+
secret: 'cNaoPYAwF60HZJzkcNaoPYAwF60HZJzk',
292+
store,
293+
cookie: { secure: false }
294+
}
295+
296+
fastify.register(fastifyCookie)
297+
fastify.register(fastifySession, options)
298+
299+
fastify.get('/', (request, reply) => {
300+
reply.send(200)
301+
})
302+
await fastify.listen(0)
303+
fastify.server.unref()
304+
305+
const { statusCode, body } = await request({
306+
url: 'http://localhost:' + fastify.server.address().port,
307+
headers: { cookie: 'sessionId=_TuQsCBgxtHB3bu6wsRpTXfjqR5sK-q_.3mu5mErW+QI7w+Q0V2fZtrztSvqIpYgsnnC8LQf6ERY;' }
308+
})
309+
t.is(statusCode, 500)
310+
t.is(JSON.parse(body).message, 'No can do')
311+
})
312+
274313
test('should not reset session cookie expiration if rolling is false', async (t) => {
275314
t.plan(3)
276315

@@ -361,8 +400,13 @@ test('should use custom sessionId generator if available (with request)', async
361400
})
362401
fastify.get('/login', (request, reply) => {
363402
request.session.returningVisitor = true
364-
request.session.regenerate()
365-
reply.status(200).send('OK ' + request.session.sessionId)
403+
request.session.regenerate(error => {
404+
if (error) {
405+
reply.status(500).send('Error ' + error)
406+
} else {
407+
reply.status(200).send('OK ' + request.session.sessionId)
408+
}
409+
})
366410
})
367411
await fastify.listen(0)
368412
fastify.server.unref()
@@ -417,8 +461,13 @@ test('should use custom sessionId generator if available (with request and rolli
417461
})
418462
fastify.get('/login', (request, reply) => {
419463
request.session.returningVisitor = true
420-
request.session.regenerate()
421-
reply.status(200).send('OK ' + request.session.sessionId)
464+
request.session.regenerate(error => {
465+
if (error) {
466+
reply.status(500).send('Error ' + error)
467+
} else {
468+
reply.status(200).send('OK ' + request.session.sessionId)
469+
}
470+
})
422471
})
423472
await fastify.listen(0)
424473
fastify.server.unref()
@@ -444,3 +493,50 @@ test('should use custom sessionId generator if available (with request and rolli
444493
t.is(response3.statusCode, 200)
445494
t.true(sessionBody3.startsWith('returningVisitor-'))
446495
})
496+
497+
test('should reload the session', async (t) => {
498+
t.plan(4)
499+
const port = await testServer((request, reply) => {
500+
request.session.someData = 'some-data'
501+
t.is(request.session.someData, 'some-data')
502+
503+
request.session.reload((err) => {
504+
t.falsy(err)
505+
506+
t.is(request.session.someData, undefined)
507+
508+
reply.send(200)
509+
})
510+
}, DEFAULT_OPTIONS)
511+
512+
const { response } = await request(`http://localhost:${port}`)
513+
514+
t.is(response.statusCode, 200)
515+
})
516+
517+
test('should save the session', async (t) => {
518+
t.plan(6)
519+
const port = await testServer((request, reply) => {
520+
request.session.someData = 'some-data'
521+
t.is(request.session.someData, 'some-data')
522+
523+
request.session.save((err) => {
524+
t.falsy(err)
525+
526+
t.is(request.session.someData, 'some-data')
527+
528+
// unlike previous test, here the session data remains after a save
529+
request.session.reload((err) => {
530+
t.falsy(err)
531+
532+
t.is(request.session.someData, 'some-data')
533+
534+
reply.send(200)
535+
})
536+
})
537+
}, DEFAULT_OPTIONS)
538+
539+
const { response } = await request(`http://localhost:${port}`)
540+
541+
t.is(response.statusCode, 200)
542+
})

types/types.d.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ declare module 'fastify' {
1010

1111
/** A session store. */
1212
sessionStore: Readonly<FastifySessionPlugin.SessionStore>;
13-
14-
/** Allows to destroy the session in the store. */
15-
destroySession(callback: (err: Error) => void): void;
1613
}
1714

1815
interface Session extends SessionData {}
@@ -29,7 +26,16 @@ interface SessionData extends ExpressSessionData {
2926
/**
3027
* Regenerates the session by generating a new `sessionId`.
3128
*/
32-
regenerate(): void;
29+
regenerate(callback: (err?: Error) => void): void;
30+
31+
/** Allows to destroy the session in the store. */
32+
destroy(callback: (err?: Error) => void): void;
33+
34+
/** Reloads the session data from the store and re-populates the request.session object. */
35+
reload(callback: (err?: Error) => void): void;
36+
37+
/** Save the session back to the store, replacing the contents on the store with the contents in memory. */
38+
save(callback: (err?: Error) => void): void;
3339

3440
/** sets values in the session. */
3541
set(key: string, value: unknown): void;

0 commit comments

Comments
 (0)