Skip to content

Commit

Permalink
commit session early for remix requests
Browse files Browse the repository at this point in the history
  • Loading branch information
jarle committed Apr 19, 2024
1 parent af16620 commit 5f72d22
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 11 deletions.
13 changes: 12 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/adapter/bin/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ processCLIArgs(process.argv.slice(2))
configure({
files: ['tests/**/*.spec.ts'],
plugins: [assert(), expect()],
filters: {
tags: ['@active'],
},
})

/*
Expand Down
5 changes: 4 additions & 1 deletion packages/adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"prepublishOnly": "npm run build",
"quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts",
"release": "npm version patch --force && npm publish",
"test": "node --loader ts-node/esm --enable-source-maps bin/test.ts",
"test": "NODE_DEBUG=adonisjs:*,matstack:* NODE_ENV=test node --loader ts-node/esm --enable-source-maps bin/test.ts",
"typecheck": "tsc --noEmit",
"version": "npm run build"
},
Expand All @@ -51,6 +51,7 @@
"@adonisjs/eslint-config": "^1.3.0",
"@adonisjs/http-server": "^7.1.0",
"@adonisjs/prettier-config": "^1.3.0",
"@adonisjs/session": "^7.4.0",
"@adonisjs/tsconfig": "^1.3.0",
"@adonisjs/vite": "^3.0.0-8",
"@japa/assert": "^2.1.0",
Expand All @@ -59,6 +60,7 @@
"@swc/core": "^1.4.14",
"@types/cloneable-readable": "^2.0.3",
"@types/node": "^20.12.7",
"@types/set-cookie-parser": "^2.4.7",
"@types/supertest": "^6.0.2",
"c8": "^9.1.0",
"copyfiles": "^2.4.1",
Expand All @@ -67,6 +69,7 @@
"node-mocks-http": "^1.14.1",
"np": "^10.0.3",
"prettier": "^3.2.5",
"set-cookie-parser": "^2.6.0",
"supertest": "^6.3.4",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
Expand Down
3 changes: 3 additions & 0 deletions packages/adapter/src/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { debuglog } from 'node:util'

export default debuglog('matstack:remix-adonisjs')
9 changes: 7 additions & 2 deletions packages/adapter/src/remix_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
createReadableStreamFromReadable,
createRequestHandler as createRemixRequestHandler,
} from '@remix-run/node'
import debug from './debug.js'
import { ReadableWebToNodeStream } from './stream_conversion.js'

export type HandlerContext = {
Expand Down Expand Up @@ -41,12 +42,13 @@ export function createRequestHandler({
let handleRequest = createRemixRequestHandler(build, mode)

return async (context: HandlerContext) => {
debug(`Creating remix request for ${context.http.request.parsedUrl}`)
const request = createRemixRequest(context.http.request, context.http.response)
const loadContext = getLoadContext(context)

const response = await handleRequest(request, loadContext)

sendRemixResponse(context.http.response, response)
sendRemixResponse(context.http, response)
}
}

Expand Down Expand Up @@ -98,10 +100,13 @@ export function createRemixRequest(req: AdonisRequest, res: AdonisResponse): Req
return new Request(url.href, init)
}

export async function sendRemixResponse(res: AdonisResponse, webResponse: Response) {
export async function sendRemixResponse(ctx: HttpContext, webResponse: Response) {
const res = ctx.response
res.response.statusMessage = webResponse.statusText
res.status(webResponse.status)

debug('Commit session early for remix response')
ctx.session?.commit()
webResponse.headers.forEach((value, key) => res.append(key, value))

if (webResponse.body) {
Expand Down
65 changes: 65 additions & 0 deletions packages/adapter/tests/cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { HttpContext } from '@adonisjs/core/http'
import type { CookieOptions } from '@adonisjs/core/types/http'

import { SessionData, SessionStoreContract } from '@adonisjs/session/types'
import debug from '../src/debug.js'

/**
* Cookie store stores the session data inside an encrypted
* cookie.
*/
export class CookieStore implements SessionStoreContract {
#ctx: HttpContext
#config: Partial<CookieOptions>

constructor(config: Partial<CookieOptions>, ctx: HttpContext) {
this.#config = config
this.#ctx = ctx
debug('initiating cookie store %O', this.#config)
}

/**
* Read session value from the cookie
*/
read(sessionId: string): SessionData | null {
debug('cookie store: reading session data %s', sessionId)

const cookieValue = this.#ctx.request.encryptedCookie(sessionId)
if (typeof cookieValue !== 'object') {
return null
}

return cookieValue
}

/**
* Write session values to the cookie
*/
write(sessionId: string, values: SessionData): void {
debug('cookie store: writing session data %s: %O', sessionId, values)
this.#ctx.response.encryptedCookie(sessionId, values, this.#config)
}

/**
* Removes the session cookie
*/
destroy(sessionId: string): void {
debug('cookie store: destroying session data %s', sessionId)
if (this.#ctx.request.cookiesList()[sessionId]) {
this.#ctx.response.clearCookie(sessionId)
}
}

/**
* Updates the cookie with existing cookie values
*/
touch(sessionId: string): void {
const value = this.read(sessionId)
debug('cookie store: touching session data %s', sessionId)
if (!value) {
return
}

this.write(sessionId, value)
}
}
71 changes: 64 additions & 7 deletions packages/adapter/tests/http_server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { RequestFactory, ResponseFactory } from '@adonisjs/http-server/factories'
import { EncryptionFactory } from '@adonisjs/core/factories/encryption'
import { CookieClient } from '@adonisjs/http-server'
import {
HttpContextFactory,
RequestFactory,
ResponseFactory,
} from '@adonisjs/http-server/factories'
import { SessionMiddlewareFactory } from '@adonisjs/session/factories'
import { SessionConfig } from '@adonisjs/session/types'
import { getActiveTest } from '@japa/runner'
import { RequestHandler } from '@remix-run/node'
import getPort from 'get-port'
import { IncomingMessage, Server, ServerResponse, createServer } from 'node:http'
import { createRemixRequest } from '../src/remix_adapter.js'
import debug from '../src/debug.js'
import { createRemixRequest, sendRemixResponse } from '../src/remix_adapter.js'
import { CookieStore } from './cookie.js'

export const httpServer = {
async create(handler: (req: IncomingMessage, res: ServerResponse) => any | Promise<any>) {
Expand All @@ -21,8 +32,54 @@ export const httpServer = {
},
}

export const remixHandler = createServer((req, res) => {
const request = new RequestFactory().merge({ req, res }).create()
const response = new ResponseFactory().merge({ req, res }).create()
return createRemixRequest(request, response)
})
export const encryption = new EncryptionFactory().create()
export const cookieClient = new CookieClient(encryption)
const sessionConfig: SessionConfig = {
enabled: true,
age: '2 hours',
clearWithBrowser: false,
cookieName: 'adonis_session',
cookie: {},
}

export const remixHandler = (requestHandler: RequestHandler) =>
createServer(async (req, res) => {
const request = new RequestFactory().merge({ req, res, encryption }).create()
const response = new ResponseFactory().merge({ req, res, encryption }).create()
const ctx = new HttpContextFactory().merge({ request, response }).create()

const middleware = await new SessionMiddlewareFactory()
.merge({
config: Object.assign(
{
store: 'cookie',
stores: {
cookie: () => new CookieStore(sessionConfig.cookie, ctx),
},
},
sessionConfig
),
})
.create()

ctx.containerResolver.bindValue('session', middleware)

await middleware.handle(ctx, async () => {
debug('Creating remix request')
const remixRequest = createRemixRequest(request, response)

debug('Creating request handler')
const foo = await requestHandler(remixRequest, {
http: ctx,
make: ctx.containerResolver.make,
})

ctx.session.commit()

debug('Sending remix response')
await sendRemixResponse(ctx, foo)
})

debug('Finishing request')
response.finish()
})
29 changes: 29 additions & 0 deletions packages/adapter/tests/session.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test } from '@japa/runner'
import { redirect } from '@remix-run/node'
import setCookieParser from 'set-cookie-parser'
import supertest from 'supertest'
import { cookieClient, remixHandler } from './http_server.js'

test.group('Session', () => {
test('commit session early for remix request', async ({ assert }) => {
let sessionId: string | undefined

const remixServer = remixHandler(async (_request, context) => {
const session = context?.http.session
session?.flash('status', 'Completed')
session?.put('username', 'jarle')
sessionId = session?.sessionId
return redirect('/admin')
})

const { headers } = await supertest(remixServer).get('/')

const cookies = setCookieParser.parse(headers['set-cookie'], { map: true })
assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), {
username: 'jarle',
__flash__: {
status: 'Completed',
},
})
})
})
29 changes: 29 additions & 0 deletions packages/reference-app/resources/remix_app/routes/login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ActionFunctionArgs, redirect } from '@remix-run/node'
import { Form } from '@remix-run/react'

export const action = async ({ context }: ActionFunctionArgs) => {
const { http } = context

http.session.put('login', 'true')

return redirect('/dashboard')
}

export default function Page() {
return (
<div className="container">
<h1>Log in</h1>
<Form method="post">
<label>
Email
<input type="email" name="email" />
</label>
<label>
Password
<input type="password" name="password" />
</label>
<button type="submit">Login</button>
</Form>
</div>
)
}

0 comments on commit 5f72d22

Please sign in to comment.