diff --git a/app/srv/api/auth/session.ts b/app/srv/api/auth/session.ts new file mode 100644 index 00000000..55da5aaf --- /dev/null +++ b/app/srv/api/auth/session.ts @@ -0,0 +1,37 @@ +import { apiContext } from "service-srv"; +import { user } from "dbgen"; +import { session } from "utils/session"; + +export const _ = { + url: "/_session", + async api() { + const { req, res } = apiContext(this); + const sdata = session.get<{ + user: user & { + org: { + id: string; + name: string; + }[]; + }; + }>(req); + if (sdata) { + let setDefaultCookie = true; + const origin = req.headers.get("origin"); + if (origin) { + const url = new URL(origin); + if (url.hostname === "localhost") { + setDefaultCookie = false; + res.setHeader("set-cookie", `${session.cookieKey}=${sdata.id};`); + } + } + + if (setDefaultCookie) { + res.setHeader( + "set-cookie", + `${session.cookieKey}=${sdata.id}; SameSite=None; Secure; HttpOnly` + ); + } + } + return sdata; + }, +}; diff --git a/app/srv/api/session.ts b/app/srv/api/session.ts new file mode 100644 index 00000000..c35adf78 --- /dev/null +++ b/app/srv/api/session.ts @@ -0,0 +1,9 @@ +import { apiContext } from "service-srv"; + +export const _ = { + url: "/session)}", + async api() { + const { req, res } = apiContext(this); + return "This is session.ts"; + } +} \ No newline at end of file diff --git a/app/srv/exports.d.ts b/app/srv/exports.d.ts index 2ebf905c..d084e701 100644 --- a/app/srv/exports.d.ts +++ b/app/srv/exports.d.ts @@ -1,4 +1,17 @@ +declare module "api/session" { + export const _: { + url: string; + api(): Promise; + }; +} declare module "exports" { + export const session: { + name: string; + url: string; + path: string; + args: any[]; + handler: Promise; + }; export const _web: { name: string; url: string; diff --git a/app/srv/exports.ts b/app/srv/exports.ts index 899dd2c6..a20e5562 100644 --- a/app/srv/exports.ts +++ b/app/srv/exports.ts @@ -1,3 +1,10 @@ +export const session = { + name: "session", + url: "/session)}", + path: "app/srv/api/session.ts", + args: [], + handler: import("./api/session") +} export const _web = { name: "_web", url: "/_web/:id/**", diff --git a/pkgs/core/index.ts b/pkgs/core/index.ts index e33a621c..9b9abcf9 100644 --- a/pkgs/core/index.ts +++ b/pkgs/core/index.ts @@ -13,6 +13,7 @@ import { prepareApiRoutes } from "./server/api-scan"; g.status = "init"; await createLogger(); +g.datadir = g.mode === "dev" ? ".data" : "../data"; g.port = parseInt(process.env.PORT || "4550"); g.mode = process.argv.includes("dev") ? "dev" : "prod"; g.log.info(g.mode === "dev" ? "DEVELOPMENT" : "PRODUCTION"); diff --git a/pkgs/core/server/api-ctx.ts b/pkgs/core/server/api-ctx.ts index fcc05e25..c3e1c189 100644 --- a/pkgs/core/server/api-ctx.ts +++ b/pkgs/core/server/api-ctx.ts @@ -1,4 +1,3 @@ -import { g } from "../utils/global"; const parseQueryParams = (ctx: any) => { const pageHref = ctx.req.url; diff --git a/pkgs/core/utils/global.ts b/pkgs/core/utils/global.ts index c3f73f39..adacd997 100644 --- a/pkgs/core/utils/global.ts +++ b/pkgs/core/utils/global.ts @@ -12,6 +12,7 @@ type SingleRoute = { export const g = global as unknown as { status: "init" | "ready"; + datadir: string; db: PrismaClient; dburl: string; mode: "dev" | "prod"; diff --git a/pkgs/core/utils/session.ts b/pkgs/core/utils/session.ts new file mode 100644 index 00000000..3cb8cd36 --- /dev/null +++ b/pkgs/core/utils/session.ts @@ -0,0 +1,106 @@ +import { createId } from "@paralleldrive/cuid2"; +import { dirAsync } from "fs-jetpack"; +import lmdb, { RootDatabase, open } from "lmdb"; +import { dirname, join } from "path"; +import { g } from "./global"; +import { dir } from "./dir"; + +const cuid = createId; +type SessionEntry = { id: string; expired: number; data: T }; + +export type SRVCache = ReturnType>; + +export const createCache = () => ({ + lmdb: null as unknown as RootDatabase>, + cookieKey: "", + async init(arg: { cookieKey: string; dbname?: string }) { + const dbpath = dir(join(g.datadir, (arg.dbname || "session") + ".lmdb")); + await dirAsync(dirname(dbpath)); + self(this).lmdb = open({ + path: dbpath, + compression: true, + }); + self(this).cookieKey = arg.cookieKey; + }, + async new(data: any, expired?: Date): Promise> { + const s = { + id: cuid(), + expired: expired ? expired.getTime() / 1000 : 0, + data, + }; + + await self(this).lmdb.put(s.id, s); + + return s; + }, + get(req: string | Request): null | SessionEntry { + let id = ""; + if (typeof req === "string") { + id = req; + } else { + const cookie = req.headers.get("cookie"); + if (cookie) { + id = parseCookies(cookie)[self(this).cookieKey]; + } + } + + if (!id) { + return null; + } + const s = self(this).lmdb.get(id) as unknown as SessionEntry; + + if (s) { + if (s.expired !== 0 && Date.now() / 1000 > s.expired) { + return null; + } + + return s; + } + return null; + }, + async set(id: string, data: any): Promise> { + await self(this).lmdb.put(id, data); + return data; + }, + del(id: string) { + return self(this).lmdb.remove(id); + }, + keys() { + return new Promise((resolve) => { + const keys: lmdb.Key[] = []; + self(this) + .lmdb.getKeys() + .forEach((e) => { + keys.push(e); + }); + resolve(keys); + }); + }, + clear() { + self(this).lmdb.clearSync(); + }, + count() { + return self(this).lmdb.getCount(); + }, +}); + +export const session = createCache(); + +const self = (me: Session) => me; +type Session = typeof session; + +export function parseCookies(cookieHeader: string) { + const list: Record = {}; + if (!cookieHeader) return list; + + cookieHeader.split(`;`).forEach(function (cookie) { + let [name, ...rest] = cookie.split(`=`); + name = name?.trim(); + if (!name) return; + const value = rest.join(`=`).trim(); + if (!value) return; + list[name] = decodeURIComponent(value); + }); + + return list; +} diff --git a/tsconfig.json b/tsconfig.json index 60ce4884..2042d68e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,9 @@ "forceConsistentCasingInFileNames": true, "allowJs": true, "paths": { + "dbgen": [ + "./node_modules/.prisma/client/index.d.ts" + ], "service-srv": [ "./pkgs/core/server/api-ctx.ts" ],