import { nanoid } from 'https://deno.land/x/nanoid@v3.0.0/async.ts'import MemoryStore from './stores/MemoryStore.ts'import CookieStore from './stores/CookieStore.ts'import type { Context } from '../deps.ts'import type Store from './stores/Store.ts'import type { CookiesGetOptions, CookiesSetDeleteOptions } from '../deps.ts'
interface SessionOptions { expireAfterSeconds?: number | null cookieGetOptions?: CookiesGetOptions cookieSetOptions?: CookiesSetDeleteOptions}
export interface SessionData { _flash: Record<string, unknown> _accessed: string | null _expire: string | null _delete: boolean [key: string]: unknown}
export default class Session {
sid: string private data: SessionData private ctx: Context
private constructor (sid : string, data : SessionData, ctx : Context) {
this.sid = sid this.data = data this.ctx = ctx }
static initMiddleware(store: Store | CookieStore = new MemoryStore(), { expireAfterSeconds = null, cookieGetOptions = {}, cookieSetOptions = {} }: SessionOptions = {}) { return async (ctx : Context, next : () => Promise<unknown>) => { const sid = await ctx.cookies.get('session', cookieGetOptions) let session: Session;
if (sid) { const sessionData = store instanceof CookieStore ? await store.getSessionByCtx(ctx) : await store.getSessionById(sid)
if (sessionData) { if (this.sessionValid(sessionData)) { session = new Session(sid, sessionData, ctx); await session.reupSession(store, expireAfterSeconds); } else { store instanceof CookieStore ? store.deleteSession(ctx) : await store.deleteSession(sid) session = await this.createSession(ctx, store, expireAfterSeconds) } } else { session = await this.createSession(ctx, store, expireAfterSeconds) }
} else { session = await this.createSession(ctx, store, expireAfterSeconds) }
ctx.state.session = session;
session.set('_accessed', new Date().toISOString()) await ctx.cookies.set('session', session.sid, cookieSetOptions)
await next()
await session.persistSessionData(store)
if (session.data._delete) { store instanceof CookieStore ? store.deleteSession(ctx) : await store.deleteSession(session.sid) } } }
private static sessionValid(sessionData: SessionData) { return sessionData._expire == null || Date.now() < new Date(sessionData._expire).getTime(); }
private async reupSession(store : Store | CookieStore, expiration : number | null | undefined) { this.data._expire = expiration ? new Date(Date.now() + expiration * 1000).toISOString() : null await this.persistSessionData(store) }
private static async createSession(ctx : Context, store : Store | CookieStore, expiration : number | null | undefined) : Promise<Session> { const sessionData = { '_flash': {}, '_accessed': new Date().toISOString(), '_expire': expiration ? new Date(Date.now() + expiration * 1000).toISOString() : null, '_delete': false }
const newID = await nanoid(21) store instanceof CookieStore ? await store.createSession(ctx, sessionData) : await store.createSession(newID, sessionData)
return new Session(newID, sessionData, ctx) }
async deleteSession() : Promise<void> { this.data._delete = true }
private persistSessionData(store : Store | CookieStore): Promise<void> | void { return store instanceof CookieStore ? store.persistSessionData(this.ctx, this.data) : store.persistSessionData(this.sid, this.data) }
get(key : string) { if (key in this.data) { return this.data[key] } else { const value = this.data['_flash'][key] delete this.data['_flash'][key] return value } }
set(key : string, value : unknown) { if(value === null || value === undefined) { delete this.data[key] } else { this.data[key] = value } }
flash(key : string, value : unknown) { this.data['_flash'][key] = value }
has(key : string) { return key in this.data || key in this.data['_flash']; }}