import {createLogger} from "@gongt/ts-stl-library/debug/create-logger"; import {LOG_LEVEL} from "@gongt/ts-stl-library/debug/levels"; import {Middleware} from "koa"; import {ObjectId} from "mongodb"; import {BaseDocument} from "../database/base.type"; import {DataModel} from "../database/mongodb"; const log = createLogger(LOG_LEVEL.SILLY, 'session'); const data = createLogger(LOG_LEVEL.DATA, 'session'); export interface Session { session: SessionObject; } export abstract class SessionDatabase extends DataModel { get(sessid: string): Promise { return this.model.findById(sessid).exec(); } set(sessid: string, $update: UpdateOperator): Promise { return this.model.update({_id: new ObjectId(sessid)}, $update, {multi: false}).exec(); } async init(session: Partial): Promise { const obj = await this.insert(session as SessionObject); return obj._id.toString(); } destroy(sessid: string): Promise { return this.remove({_id: new ObjectId(sessid)}); } } export type UpdateOperator = { $set?: Partial; $unset?: Record; } export interface SessionOptions { cookieName?: string; timeoutMs?: number; } const defaultOptions: SessionOptions = { cookieName: 'NODE_SESSID', timeoutMs: 12 * 60 * 60 * 1000, }; export function kMongooseSession (db: SessionDatabase, options: SessionOptions = {}): Middleware { const {cookieName, timeoutMs} = Object.assign({}, defaultOptions, options); const cookieSetOpt = { maxAge: timeoutMs, path: '/', secure: false, // allow http httpOnly: true, signed: true, overwrite: true, }; const cookieTimeoutOpt = { ...cookieSetOpt, maxAge: 0, }; return async function kMongooseSession(ctx, next) { const sessId = ctx.cookies.get(cookieName); let changed = false; let update: any = { $set: {}, $unset: {}, }; const sessData = sessId? await db.get(sessId) || {} : {} as any; log('init session: %s', sessId); data(sessData); const session = new Proxy(sessData, { set(target, p: PropertyKey, value: any, receiver: any): boolean { if (!changed && target[p] !== value) { changed = true; } target[p] = value; if (update.$unset) { delete update.$unset[p]; } if (!update.$set) { update.$set = {}; } update.$set[p] = value; return true; }, deleteProperty(target: SessionObject, p: PropertyKey): boolean { if (!changed && target.hasOwnProperty(p)) { changed = true; } delete target[p]; if (update.$set) { delete update.$set[p]; } if (!update.$unset) { update.$unset = {}; } update.$unset[p] = 1; return true; }, }); ctx.session = session; await next(); if (!ctx.hasOwnProperty('session')) { // delete session if (sessId) { await db.destroy(sessId); log('remove session cookie: %s', sessId); ctx.cookies.set(cookieName, undefined, cookieTimeoutOpt); } } else if (ctx.session !== session) { // replace session let newId: string; if (sessId) { log('session replaced.'); await db.destroy(sessId); newId = await db.init(ctx.session) } else { log('new session created.'); newId = await db.init(ctx.session) } log('write session id to cookie: %s', newId); data(ctx.session); ctx.cookies.set(cookieName, newId, cookieSetOpt); } else if (changed) { await db.set(sessId, update); log('renew session cookie: %s', sessId); ctx.cookies.set(cookieName, sessId, cookieSetOpt); } }; }