Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class SessionClient {

debug('committing session data during api request')
if (!this.#valuesStore.isEmpty) {
this.#store.write(this.sessionId, this.#valuesStore.toJSON())
await this.#store.write(this.sessionId, this.#valuesStore.toJSON())
}
}

Expand All @@ -112,7 +112,7 @@ export class SessionClient {
*/
async destroy(sessionId?: string) {
debug('destroying session data during api request')
this.#store.destroy(sessionId || this.sessionId)
await this.#store.destroy(sessionId || this.sessionId)
}

/**
Expand Down
8 changes: 7 additions & 1 deletion src/define_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,12 @@ export const stores: {
return configProvider.create(async (app) => {
const { RedisStore } = await import('./stores/redis.js')
const redis = await app.container.make('redis')
const encryption = config.encrypt ? await app.container.make('encryption') : undefined

return (_, sessionConfig: SessionConfig) => {
return new RedisStore(redis.connection(config.connection), sessionConfig.age)
return new RedisStore(redis.connection(config.connection), sessionConfig.age, {
encryption,
})
}
})
},
Expand Down Expand Up @@ -251,10 +254,13 @@ export const stores: {
)
}

const encryption = config?.encrypt ? await app.container.make('encryption') : undefined

return (_, sessionConfig: SessionConfig) => {
return new DatabaseStore(db.connection(connectionName), sessionConfig.age, {
tableName: config?.tableName,
gcProbability: config?.gcProbability,
encryption,
})
}
})
Expand Down
19 changes: 13 additions & 6 deletions src/plugins/japa/api_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import type { ApplicationService } from '@adonisjs/core/types'
import { ApiClient, ApiRequest, ApiResponse } from '@japa/api-client'

import { SessionClient } from '../../client.ts'
import type { SessionData } from '../../types.ts'
import { createTestStore } from './store.ts'
import type { ResolvedSessionConfig, SessionData } from '../../types.ts'

declare module '@japa/api-client' {
export interface ApiRequest {
Expand Down Expand Up @@ -127,7 +128,7 @@ export const sessionApiClient = (app: ApplicationService) => {
/**
* Resolve config from the provider
*/
const config = await configProvider.resolve<any>(app, sessionConfigProvider)
const config = await configProvider.resolve<ResolvedSessionConfig>(app, sessionConfigProvider)
if (!config) {
throw new RuntimeException(
'Invalid "config/session.ts" file. Make sure you are using the "defineConfig" method'
Expand All @@ -142,7 +143,7 @@ export const sessionApiClient = (app: ApplicationService) => {
ApiRequest.getter(
'sessionClient',
function () {
return new SessionClient(config.stores.memory())
return new SessionClient(createTestStore(config))
},
true
)
Expand Down Expand Up @@ -242,13 +243,19 @@ export const sessionApiClient = (app: ApplicationService) => {
})

request.teardown(async (response) => {
const sessionId = response.cookie(config.cookieName)
/**
* With "refreshCookie" disabled, the response has no session
* cookie when the session id did not change.
*/
const sessionId =
response.cookie(config.cookieName)?.value ??
(config.refreshCookie === false ? request.sessionClient.sessionId : undefined)

/**
* Reading session data from the response cookie
*/
response.sessionBag = sessionId
? await response.request.sessionClient.load(sessionId.value)
? await response.request.sessionClient.load(sessionId)
: {
values: {},
flashMessages: {},
Expand All @@ -257,7 +264,7 @@ export const sessionApiClient = (app: ApplicationService) => {
/**
* Cleanup state
*/
await request.sessionClient.destroy(sessionId?.value)
await request.sessionClient.destroy(sessionId)
})
})
}
Expand Down
17 changes: 11 additions & 6 deletions src/plugins/japa/browser_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import type { ApplicationService } from '@adonisjs/core/types'
import type { CookieOptions as AdonisCookieOptions } from '@adonisjs/core/types/http'

import { SessionClient } from '../../client.ts'
import type { SessionConfig, SessionData } from '../../types.ts'
import { createTestStore } from './store.ts'
import type { ResolvedSessionConfig, SessionConfig, SessionData } from '../../types.ts'

declare module 'playwright' {
export interface BrowserContext {
Expand Down Expand Up @@ -115,7 +116,7 @@ export const sessionBrowserClient = (app: ApplicationService) => {
/**
* Resolve config from the provider
*/
const config = await configProvider.resolve<any>(app, sessionConfigProvider)
const config = await configProvider.resolve<ResolvedSessionConfig>(app, sessionConfigProvider)
if (!config) {
throw new RuntimeException(
'Invalid "config/session.ts" file. Make sure you are using the "defineConfig" method'
Expand All @@ -127,7 +128,7 @@ export const sessionBrowserClient = (app: ApplicationService) => {
/**
* Reference to session client per browser context
*/
context.sessionClient = new SessionClient(config.stores.memory())
context.sessionClient = new SessionClient(createTestStore(config))

/**
* Initiating session store
Expand Down Expand Up @@ -183,11 +184,15 @@ export const sessionBrowserClient = (app: ApplicationService) => {
}

/**
* Destroy session when context is closed
* Destroy session when context is closed. Playwright does not wait
* for "close" listeners, so the destroy runs inside close() to
* finish before it resolves.
*/
context.on('close', async function () {
const close = context.close.bind(context)
context.close = async function (options) {
await context.sessionClient.destroy()
})
return close(options)
}
},
})
}
Expand Down
35 changes: 35 additions & 0 deletions src/plugins/japa/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* @adonisjs/session
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { MemoryStore } from '../../stores/memory.ts'
import { CookieStore } from '../../stores/cookie.ts'
import type { ResolvedSessionConfig, SessionStoreContract } from '../../types.ts'

/**
* Creates the store the test clients write sessions to: the store the
* application reads them from. The cookie store needs an HTTP request,
* so the clients fall back to the memory store for it.
*
* A store serves one request and remembers the sessions it read, which
* changes how it writes them later. Each call gets a new store, so a
* client that read a session can still write it after a logout.
*/
export function createTestStore(config: ResolvedSessionConfig): SessionStoreContract {
const createStore = () => {
const store = config.stores[config.store](null as any, config)
return store instanceof CookieStore ? new MemoryStore() : store
}

return {
read: (sessionId) => createStore().read(sessionId),
write: (sessionId, data) => createStore().write(sessionId, data),
destroy: (sessionId) => createStore().destroy(sessionId),
touch: (sessionId) => createStore().touch(sessionId),
}
}
18 changes: 16 additions & 2 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,17 @@ export class Session extends Macroable {
const contents = await this.#store.read(this.#sessionId)
this.#valuesStore = new ValuesStore(contents)

/**
* The cookie names a session the store does not have (expired or
* destroyed). With "renewMissingSessionId", start a fresh session
* under a new id, so the id of an ended session is never used again.
*/
if (this.config.renewMissingSessionId && !contents && this.#sessionIdFromCookie) {
debug('session %s not found, starting a new session', this.#sessionIdFromCookie)
this.#sessionIdFromCookie = undefined
this.#sessionId = randomUUID()
}

/**
* Extract flash messages from the store and keep a local
* copy of it.
Expand Down Expand Up @@ -688,9 +699,12 @@ export class Session extends Macroable {
debug('committing session data')

/**
* Touch the session id cookie to stay alive
* Touch the session id cookie to stay alive. With "refreshCookie"
* disabled, only send it when the browser does not have this id.
*/
this.#ctx.response.cookie(this.config.cookieName, this.#sessionId, this.config.cookie!)
if (this.config.refreshCookie !== false || this.#sessionId !== this.#sessionIdFromCookie) {
this.#ctx.response.cookie(this.config.cookieName, this.#sessionId, this.config.cookie!)
}

/**
* Delete the session data when the session store
Expand Down
14 changes: 11 additions & 3 deletions src/session_collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,24 @@ import type {
* ```
*/
export class SessionCollection {
#store: SessionStoreWithTaggingContract
#config: ResolvedSessionConfig

/**
* Creates a new SessionCollection instance
*
* @param config - Resolved session configuration
*/
constructor(config: ResolvedSessionConfig) {
const storeFactory = config.stores[config.store]
this.#store = storeFactory(null as any, config) as SessionStoreWithTaggingContract
this.#config = config
}

/**
* A store serves one request and remembers the sessions it read.
* The collection is a singleton, so each operation gets a new store.
*/
get #store(): SessionStoreWithTaggingContract {
const storeFactory = this.#config.stores[this.#config.store]
return storeFactory(null as any, this.#config) as SessionStoreWithTaggingContract
}

/**
Expand Down
Loading
Loading