Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

The basics

HTTP session

HTTP is stateless, so the session keeps what your app knows about a user from one request to the next: who is signed in, a cart, a status message to show once.

Introduction#

Oriel stores sessions in your database by default, and can keep them in files, in a cookie or in Redis instead.

The StartSession middleware, in the web group, loads the session before your route runs and saves it once the response is ready, so every route in routes/web.ts has one. The api group has no StartSession: a route in routes/api.ts gets an empty session that is never saved.

Configuration#

The session's settings are in config/session.ts. A new app uses the database driver:

config/session.ts
import { dirname, join } from 'node:path'import { env, Str } from '@orieljs/framework'const basePath = dirname(import.meta.dirname)export default {  driver: env('SESSION_DRIVER', 'database'),  lifetime: Number(env('SESSION_LIFETIME', 120)),  expire_on_close: false,  encrypt: false,  files: join(basePath, 'storage/framework/sessions'),  connection: env('SESSION_CONNECTION'),  table: 'sessions',  lottery: [2, 100],  cookie: env('SESSION_COOKIE', Str.slug(String(env('APP_NAME', 'oriel')), '_') + '_session'),  path: '/',  domain: env('SESSION_DOMAIN', null),  secure: env('SESSION_SECURE_COOKIE', null),  http_only: true,  same_site: 'lax',}

driver says where the sessions are stored:

  • database: in the sessions table of your database.
  • file: one file per session, in storage/framework/sessions (the files setting).
  • cookie: the whole session in an encrypted cookie.
  • redis: in Redis.
  • array: in memory, in the app's process. Your tests use it.

lifetime is the number of minutes a session lasts without a request, 120 by default. With expire_on_close, the cookie lasts until the browser closes. encrypt encrypts the session's data with your APP_KEY before it is stored.

cookie names the session cookie, after your app's name: blog_session for an app called Blog. path, domain, http_only and same_site are the cookie's attributes. secure sends the cookie over HTTPS only; when it is null, as in a new app, the cookie is secure when the request came over HTTPS.

lottery cleans up expired sessions: on average, 2 requests in 100 delete the sessions older than lifetime from the database, the files or the memory. Redis and cookie sessions expire on their own.

Driver prerequisites#

Database#

The database driver needs the sessions table, which a new app's first migration creates, with the users table:

await Schema.create('sessions', (t) => {  t.string('id').primary()  t.foreignId('user_id').nullable().index()  t.string('ip_address', 45).nullable()  t.text('user_agent').nullable()  t.longText('payload')  t.integer('last_activity').index()})

Each row also records the signed-in user's id, the IP address and the user agent of the session's last request. connection names the database connection to use, the default one when it is null, and table the table.

Redis#

The redis driver talks to the Redis server of config/database.ts: connection names an entry of database.redis (default unless you set SESSION_CONNECTION). Its keys start with oriel_session: and expire after lifetime. Oriel's Redis client, ioredis, comes with the framework, and the connection is closed when your app shuts down.

Interacting with the session#

Retrieving data#

You reach the session through the request, or with the session() helper. Through the request:

app/Http/Controllers/UserController.ts
import { request, view } from '@orieljs/framework'import { User } from '../../Models/User'import { Controller } from './Controller'export class UserController extends Controller {  async show(id: string) {    const value = request().session().get('key')    // ...    return view('user.profile', { user: await User.findOrFail(id) })  }}

get() takes a default, returned when the key isn't in the session, and a type for the value, which is a cast, not a check:

const value = request().session().get('key', 'default')const theme = request().session().get<string>('theme')

Keys take dots: get('user.name') reads the name of the object stored under user.

The global session helper#

session() with a key returns its value, with a default if you give one. With an object, it stores the object's values. With nothing, it returns the session itself:

Route.get('/home', () => {  // Retrieve a piece of data from the session...  const value = session('key')  // Specifying a default value...  const withDefault = session('key', 'default')  // Store a piece of data in the session...  session({ key: 'value' })  return { value, withDefault, all: session().all() }})

The Session facade does the same: Session.get('key'), Session.put('key', 'value'). The three reach the same session, the current request's, and your tests check them all with assertSessionHas().

Retrieving all session data#

const data = request().session().all()

Retrieving a portion of the session data#

only() and except() take a list of keys:

const data = request().session().only(['username', 'email'])const others = request().session().except(['username', 'email'])

Determining if an item exists in the session#

has() tells you whether a key is present and not null:

if (request().session().has('users')) {  // ...}

exists() tells you whether a key is present, even with a null value, and missing() whether it isn't. Both take several keys:

if (request().session().exists('users')) {  // ...}if (request().session().missing('users')) {  // ...}

Storing data#

Store a value with put(), or with the session() helper. put() also takes an object of values:

// Via the request...request().session().put('key', 'value')// Via the global "session" helper...session({ key: 'value' })

The session is stored as JSON: a Date comes back as its ISO string on the next request, and a class instance, a model for one, as a plain object. Store ids and plain values, and look models up again.

Pushing to array session values#

push() adds a value to the list stored under a key, and starts the list when there is none:

request().session().push('user.teams', 'developers')

Retrieving and deleting an item#

pull() returns a value and removes it from the session:

const value = request().session().pull('key', 'default')

Incrementing and decrementing session values#

increment() and decrement() change the number stored under a key, by 1 or by the amount you give, and return the new value. A missing key counts as 0:

request().session().increment('count')request().session().increment('count', 2)request().session().decrement('count')request().session().decrement('count', 2)

Flash data#

flash() stores a value for this request and the next one only, which suits a status message shown once after a redirect:

request().session().flash('status', 'Your post was published.')

reflash() keeps all of the flash data for one more request, and keep() keeps only the keys you name:

request().session().reflash()request().session().keep('username', 'email')

now() stores a value for the current request only:

request().session().now('status', 'Your post was published.')

A redirect flashes data too: redirect('/dashboard').with('status', 'Profile updated.'). The input a failed validation sends back, which old() reads, and its errors, which errors() reads, are flash data as well (see Requests and Validation).

Deleting data#

forget() removes keys from the session, and flush() removes everything:

// Forget a single key...request().session().forget('name')// Forget multiple keys...request().session().forget('name', 'status')request().session().flush()

remove() removes a key, as forget() does, and returns its value, as pull() does:

const name = request().session().remove<string>('name')

forget() and keep() take the keys as separate arguments, where Laravel's take a list too.

Regenerating the session ID#

Regenerating the session's ID stops a session fixation attack, in which an attacker gets a user to sign in with a session ID the attacker already knows. The starter kits regenerate it when a user signs in. To do it yourself, call regenerate(), which also gives the session a new CSRF token:

await request().session().regenerate()

regenerate(true) also deletes the old session from the store. invalidate() removes all the data from the session and gives it a new ID, deleting the old one. As it removes the CSRF token with the rest, call regenerateToken() after it, as the starter kits do when a user signs out:

await request().session().invalidate()request().session().regenerateToken()

The CSRF token#

The session holds the CSRF token that protects your forms. token() returns it, as csrfToken() does, and regenerateToken() replaces it:

const token = request().session().token()

CSRF protection explains how the token is sent and checked.

Adding custom session drivers#

Implementing the driver#

A custom driver is a class that implements the SessionHandler contract. A stubbed MongoDB driver looks like this:

app/Extensions/MongoSessionHandler.ts
import type { SessionHandler } from '@orieljs/framework/contracts'export class MongoSessionHandler implements SessionHandler {  async read(id: string): Promise<string> {    return ''  }  async write(id: string, data: string): Promise<void> {}  async destroy(id: string): Promise<void> {}  async gc(maxLifetimeSeconds: number): Promise<void> {}}

Oriel has no folder for such classes: put them where you like, as app/Extensions here. What each method does:

  • read() returns the session's data, as the string write() stored, or '' when there is none or it has expired. Oriel turns the data into JSON and back itself.
  • write() stores the data string under the session's ID.
  • destroy() deletes a session.
  • gc() deletes the sessions not written for more than maxLifetimeSeconds seconds. A store that expires its entries on its own can leave it empty.

Registering the driver#

Register the driver with Session.extend(), in a service provider's boot(). The callback receives the request's container and the session's configuration, and returns a handler; it runs for each request's session:

app/Providers/AppServiceProvider.ts
import { ServiceProvider, Session } from '@orieljs/framework'import { MongoSessionHandler } from '../Extensions/MongoSessionHandler'export class AppServiceProvider extends ServiceProvider {  override boot(): void {    Session.extend('mongo', () => new MongoSessionHandler())  }}

Then set SESSION_DRIVER=mongo in your .env, or driver in config/session.ts.

Testing#

Your tests run with the array driver, and the session carries over from one request of a test to the next. withSession() puts data in it before a request (HTTP tests covers the rest):

tests/Feature/Cart.test.ts
import { test } from 'vitest'import { TestCase } from '../TestCase'const t = TestCase.boot()test('the cart remembers its items', async () => {  const response = await t.withSession({ cart: ['apple'] }).post('/cart', { item: 'pear' })  response.assertSessionHas('cart', ['apple', 'pear']).assertSessionMissing('coupon')})

flushSession() empties the session: the next request starts with none, so a user who signed in on an earlier request is signed out. A user you gave actingAs() stays signed in. withSession() called after a request sets its keys over the ones the stored session holds.

A response's session assertions check the session as the request left it, flash data included:

AssertionPasses when
assertSessionHas(key)The key is in the session and not null
assertSessionHas(key, value)The key holds that value; value may also be a function that returns true
assertSessionHas([keys])Every key is in the session and not null
assertSessionHasAll({ key: value })Every key holds its value
assertSessionMissing(key)The key isn't in the session, or holds null
assertSessionHasInput(key)The key is in the old input
assertSessionHasErrors(keys)The session has validation errors for those keys
assertSessionHasNoErrors()The session has no validation errors

response.session() returns the session itself, for anything else.

Privacy policy ·

esc