From 46d5d1d7778201fee533507d0c6abdced2ddcc5b Mon Sep 17 00:00:00 2001 From: rizky Date: Wed, 28 Aug 2024 03:45:00 -0700 Subject: [PATCH] fix lib --- server/context.ts | 2 +- server/server-route.ts | 2 +- server/session/client-session.ts | 49 ---- server/session/server-session.ts | 50 ---- session/client-session.ts | 223 +++++++++++++++++ .../router/session-check.ts | 9 +- .../router/session-router.ts | 0 session/server-session.ts | 234 ++++++++++++++++++ .../session => session}/store/client-store.ts | 10 + {server/session => session}/store/schema.ts | 1 + .../store/session-store.ts | 96 ++++--- {server/session => session}/type.ts | 30 ++- utils/client-session.ts | 80 ------ 13 files changed, 564 insertions(+), 222 deletions(-) delete mode 100755 server/session/client-session.ts delete mode 100755 server/session/server-session.ts create mode 100755 session/client-session.ts rename {server/session => session}/router/session-check.ts (61%) rename {server/session => session}/router/session-router.ts (100%) create mode 100755 session/server-session.ts rename {server/session => session}/store/client-store.ts (57%) rename {server/session => session}/store/schema.ts (95%) rename {server/session => session}/store/session-store.ts (58%) rename {server/session => session}/type.ts (70%) delete mode 100755 utils/client-session.ts diff --git a/server/context.ts b/server/context.ts index 50a1138..b32c824 100755 --- a/server/context.ts +++ b/server/context.ts @@ -1,4 +1,4 @@ -import { ServerContext, SessionContext } from "./server-route"; +import type { ServerContext, SessionContext } from "lib/session/type"; export const sessionContext = (sf: any) => { return sf as unknown as SessionContext; diff --git a/server/server-route.ts b/server/server-route.ts index 0ebaf1f..507d6c9 100755 --- a/server/server-route.ts +++ b/server/server-route.ts @@ -1,6 +1,6 @@ import { _post } from "lib/utils/post"; import { addRoute, createRouter, findRoute } from "rou3"; -import { ServerContext, SessionContext } from "./session/type"; +import { ServerContext, SessionContext } from "../session/type"; type RouteFn = (...arg: any[]) => Promise; diff --git a/server/session/client-session.ts b/server/session/client-session.ts deleted file mode 100755 index 46a8335..0000000 --- a/server/session/client-session.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { newClientRouter } from "../server-route"; -import { sessionRouter } from "./router/session-router"; -import { sessionClientStore } from "./store/client-store"; -import { ClientSession } from "./type"; - -export const newClientSession = (arg?: { - on: Partial<{ - messageReceived: (session: ClientSession) => Promise; - afterLogin: (session: ClientSession) => Promise; - afterLogout: (session: ClientSession) => Promise; - afterRecheck: (session: ClientSession) => Promise; - }>; -}) => { - const store = sessionClientStore(); - const client = newClientRouter(sessionRouter); - - const session: ClientSession = { - status: "checking", - current: null, - async connect() { - const url = new URL(location.href); - url.protocol = "wss:"; - const ws = new WebSocket(url); - - ws.onopen = () => { - ws.send("ok"); - }; - ws.onmessage = (m) => { - console.log(m); - }; - // const current = await store.load(); - // if (!current) { - // this.status = "guest"; - // } else { - // this.status = await client.check(current.uid, current.sid); - // } - return { status: this.status }; - }, - async login(arg: { - method: "user-pass"; - username: string; - password: string; - }) {}, - async logout() {}, - }; - - session.connect(); - return session; -}; diff --git a/server/session/server-session.ts b/server/session/server-session.ts deleted file mode 100755 index eb8027f..0000000 --- a/server/session/server-session.ts +++ /dev/null @@ -1,50 +0,0 @@ -/// - -import { useServerRouter } from "../server-route"; -import { sessionRouter } from "./router/session-router"; -import { newSessionStore } from "./store/session-store"; -import { ServerContext } from "./type"; - -type SessionServerHandler = { - cleanup: () => Promise; - handle: (arg: ServerContext) => Promise; -}; - -export const createSessionServer = (arg: { - encrypt?: boolean; - server_router?: ReturnType; - site_id?: string; -}): SessionServerHandler => { - const session_store = newSessionStore(arg.site_id); - - const session_router = useServerRouter(sessionRouter); - - const server_handler: SessionServerHandler = { - async cleanup() {}, - async handle(server_arg) { - const { req, handle, url } = server_arg; - - const route_arg = { - ...server_arg, - session: { - ...session_store, - current: undefined, - }, - }; - - if (url.pathname.startsWith("/_session/")) { - const res = await session_router.handle(route_arg); - if (res) return res; - } - - if (arg.server_router) { - const res = await arg.server_router.handle(route_arg); - if (res) return res; - } - - return handle(req); - }, - }; - - return server_handler; -}; diff --git a/session/client-session.ts b/session/client-session.ts new file mode 100755 index 0000000..de7d1f9 --- /dev/null +++ b/session/client-session.ts @@ -0,0 +1,223 @@ +import { ConsoleLogWriter } from "drizzle-orm"; +import { newClientRouter } from "../server/server-route"; +import { sessionRouter } from "./router/session-router"; +import { sessionClientStore } from "./store/client-store"; +import { ClientSession, SessionAuth, SessionData } from "./type"; + +export const newClientSession = (arg?: { + on?: Partial<{ + messageReceived: (session: ClientSession) => Promise; + afterLogin: (session: ClientSession) => Promise; + afterLogout: (session: ClientSession) => Promise; + afterInit: (session: ClientSession) => Promise; + }>; +}) => { + const store = sessionClientStore(); + const client = newClientRouter(sessionRouter); + + const login_promise = { resolve: null as any, reject: null as any }; + const logout_promise = { resolve: null as any, reject: null as any }; + + const session: ClientSession = { + status: "checking", + wsid: "", + current: null, + connected: false, + get connectURL() { + const url = new URL(location.href); + url.protocol = "wss:"; + url.hash = ""; + return url; + }, + connect(auth?: SessionAuth) { + return new Promise(async (done, reject) => { + const current = this.current; + if (current || auth) { + if (this.ws) { + await wsReady(this.ws); + } + + const ws = new WebSocket(this.connectURL); + this.ws = ws; + ws.onclose = () => { + session.connected = false; + console.log(this.status); + if (this.status === "logout") { + store.clear(); + this.current = null; + if (arg?.on?.afterLogout) { + arg.on.afterLogout(session); + } + logout_promise.resolve(); + logout_promise.resolve = null; + logout_promise.reject = null; + } else { + setTimeout(() => { + console.warn("Reconnecting Session WS..."); + this.connect(); + }, 2000); + } + }; + ws.onopen = () => { + if (session.current) { + ws.send( + JSON.stringify({ + uid: session.current.uid, + sid: session.current.sid, + }) + ); + } else { + if (auth) { + ws.send(JSON.stringify(auth)); + } else { + if (ws.readyState === ws.OPEN) { + ws.close(); + } + } + } + }; + ws.onmessage = async (m) => { + if (!session.connected) { + try { + const parsed = JSON.parse(m.data) as + | { + status: "ok"; + wsid: string; + session: SessionData; + } + | { status: "failed" }; + + if (parsed.status === "ok") { + session.wsid = parsed.wsid; + session.current = parsed.session; + if (login_promise.resolve) { + session.connected = true; + login_promise.resolve(session.current); + await store.save(session.current); + } + } else { + if (login_promise.reject) login_promise.reject(); + } + login_promise.resolve = null; + login_promise.reject = null; + if (arg?.on?.afterLogin) { + arg.on.afterLogin(session); + } + done(); + + return; + } catch (e) { + reject(e); + } + + if (ws.readyState === ws.OPEN) { + ws.close(); + } + } + }; + } else { + done(); + } + }); + }, + async init() { + const current = await store.load(); + if (!current) { + this.status = "guest"; + } else { + this.current = current; + this.status = await client.check(current.uid, current.sid); + if (this.status !== "active") { + await store.clear(); + this.current = null; + } else { + await this.connect(); + } + } + if (arg?.on?.afterInit) { + arg.on.afterInit(session); + } + + return { status: this.status }; + }, + login(auth: SessionAuth) { + return new Promise>(async (resolve, reject) => { + if (isEditor) { + resolve({} as any); + return; + } + + login_promise.resolve = resolve; + login_promise.reject = reject; + if (this.status === "checking") { + await new Promise((done) => { + const ival = setInterval(() => { + if (this.status !== "checking") { + clearInterval(ival); + done(); + } + }, 100); + }); + } + + if (this.status === "guest") { + this.connect(auth); + } else { + console.error( + `\ +Session login failed, current status is: ${this.status}. +Login is prevented, please logout first before re-login!` + ); + if (this.current) { + resolve(this.current); + } else { + if (auth) { + this.connect(auth); + } else { + reject("Current session not found"); + } + } + } + }); + }, + logout() { + return new Promise(async (resolve, reject) => { + if (isEditor) { + resolve({} as any); + return; + } + + logout_promise.resolve = resolve; + logout_promise.reject = reject; + if (this.status === "active" && this.ws) { + await wsReady(this.ws); + this.status = "logout"; + + this.ws.send(JSON.stringify({ action: "logout" })); + } + }); + }, + }; + + if (!isEditor) { + session.init(); + } + return session; +}; + +const wsReady = async (ws: WebSocket) => { + if (ws) { + if (ws.readyState === ws.OPEN) return; + else { + ws.close(); + await new Promise((done) => { + const ival = setInterval(() => { + if (ws.readyState === ws.CLOSED) { + clearInterval(ival); + done(); + } + }, 100); + }); + } + } +}; diff --git a/server/session/router/session-check.ts b/session/router/session-check.ts similarity index 61% rename from server/session/router/session-check.ts rename to session/router/session-check.ts index 3526f42..290694e 100755 --- a/server/session/router/session-check.ts +++ b/session/router/session-check.ts @@ -4,8 +4,15 @@ import { ClientSessionStatus } from "../type"; export default async function (this: any, uid: string, sid: string) { const ctx = sessionContext(this); - let result = "invalid" as ClientSessionStatus; + const session = ctx.session.findFirst({ uid, sid }); + if (session) { + if (!session.expired_at || session.expired_at > Date.now()) { + result = "active"; + } else { + result = "expired"; + } + } return result; } diff --git a/server/session/router/session-router.ts b/session/router/session-router.ts similarity index 100% rename from server/session/router/session-router.ts rename to session/router/session-router.ts diff --git a/session/server-session.ts b/session/server-session.ts new file mode 100755 index 0000000..34ed441 --- /dev/null +++ b/session/server-session.ts @@ -0,0 +1,234 @@ +/// + +import { ServerWebSocket } from "bun"; +import { useServerRouter } from "../server/server-route"; +import { sessionRouter } from "./router/session-router"; +import { newSessionStore } from "./store/session-store"; +import { + ServerContext, + SessionAuth, + SessionStore, + SingleSession, +} from "./type"; +import { createId } from "@paralleldrive/cuid2"; +import { ConsoleLogWriter } from "drizzle-orm"; + +type WS = ServerWebSocket<{ url: string }>; +type SessionServerHandler = { + cleanup: () => Promise; + handle: (arg: ServerContext) => Promise; + ws: { + sids: Record; + index: Record; + conns: Map; + init: () => PrasiServer["ws"]; + }; +}; + +export const initSessionServer = ( + server: PrasiServer, + arg: { + encrypt?: boolean; + router?: ReturnType; + login: ( + session: SessionStore, + arg: SessionAuth + ) => Promise | false>; + } +) => { + try { + const session_store = newSessionStore(server.site_id); + const session_router = useServerRouter(sessionRouter); + const server_handler: SessionServerHandler = { + async cleanup() {}, + ws: { + sids: {}, + index: {}, + conns: new Map(), + init() { + return { + async message(ws, message) { + const sids = server_handler.ws.sids; + const conns = server_handler.ws.conns; + const index = server_handler.ws.index; + const conn = conns.get(ws); + if (conn) { + try { + if (typeof message === "string") { + const parsed = JSON.parse(message) as { action: "logout" }; + + if (parsed.action === "logout") { + const sid = `${conn.uid}-${conn.sid}`; + + if (!sids[sid]) { + sids[sid] = []; + } + sids[sid].forEach((e) => { + conns.delete(e); + e.close(); + }); + + session_store.update( + { sid: conn.sid, uid: conn.uid }, + { + active: false, + wsid: [], + } + ); + } + } + } catch (e) {} + } else { + try { + if (typeof message === "string") { + const activateSession = (result: SingleSession) => { + try { + const wsid = createId(); + conns.set(ws, { + wsid, + uid: result.uid, + sid: result.sid, + }); + + index[wsid] = ws; + const sid = `${result.uid}-${result.sid}`; + + if (!sids[sid]) { + sids[sid] = []; + } + + sids[sid].push(ws); + + const wsids = Object.values(sids[sid]).map( + (e) => server_handler.ws.conns.get(e)?.sid || "" + ); + + session_store.update( + { sid: result.sid, uid: result.uid }, + { + wsid: wsids, + } + ); + + ws.send( + JSON.stringify({ + status: "ok", + wsid, + session: result, + }) + ); + } catch (e) { + console.error(e); + } + }; + + const parsed = JSON.parse(message) as + | { + method: undefined; + uid: string; + sid: string; + } + | SessionAuth; + + if (parsed) { + if (parsed.method) { + if (parsed.method === "user-pass") { + const result = await arg.login(session_store, parsed); + if (result) { + activateSession(result); + } else { + ws.send(JSON.stringify({ status: "failed" })); + } + } + } else { + const result = session_store.findFirst({ + uid: parsed.uid, + sid: parsed.sid, + }); + + if (result) { + if ( + result.active && + (!result.expired_at || + (result.expired_at && + result.expired_at > Date.now())) + ) { + activateSession(result); + } else { + session_store.update( + { + uid: parsed.uid, + sid: parsed.sid, + }, + { active: false } + ); + + ws.send(JSON.stringify({ status: "expired" })); + } + } + } + } + } + } catch (e) { + console.error(e); + } + } + }, + close(ws, code, reason) { + const result = server_handler.ws.conns.get(ws); + + if (result) { + const sid = `${result.uid}-${result.sid}`; + if (server_handler.ws.sids[sid]) { + server_handler.ws.sids[sid] = server_handler.ws.sids[ + sid + ].filter((e) => e !== ws); + + session_store.update( + { sid: result.sid }, + { + wsid: Object.values(server_handler.ws.sids[sid]).map( + (e) => server_handler.ws.conns.get(e)?.sid || "" + ), + } + ); + } + + delete server_handler.ws.index[result.wsid]; + server_handler.ws.conns.delete(ws); + } + }, + }; + }, + }, + async handle(server_arg) { + const { req, handle, url } = server_arg; + + const route_arg = { + ...server_arg, + session: { + ...session_store, + current: undefined, + }, + }; + + if (url.pathname.startsWith("/_session/")) { + const res = await session_router.handle(route_arg); + if (res) return res; + } + + if (arg.router) { + const res = await arg.router.handle(route_arg); + if (res) return res; + } + + return handle(req); + }, + }; + + server.ws = server_handler.ws.init(); + server.session = server_handler; + } catch (e) { + console.log(e); + } +}; diff --git a/server/session/store/client-store.ts b/session/store/client-store.ts similarity index 57% rename from server/session/store/client-store.ts rename to session/store/client-store.ts index e478a66..1c88323 100755 --- a/server/session/store/client-store.ts +++ b/session/store/client-store.ts @@ -12,4 +12,14 @@ export const sessionClientStore = () => ({ } return null; }, + async save(arg: SessionData): Promise { + if (w._prasi?.site_id) { + localStorage.setItem(`sid-${w._prasi?.site_id}`, JSON.stringify(arg)); + } + }, + async clear(): Promise { + if (w._prasi?.site_id) { + localStorage.removeItem(`sid-${w._prasi?.site_id}`); + } + }, }); diff --git a/server/session/store/schema.ts b/session/store/schema.ts similarity index 95% rename from server/session/store/schema.ts rename to session/store/schema.ts index cc99455..352062b 100755 --- a/server/session/store/schema.ts +++ b/session/store/schema.ts @@ -14,6 +14,7 @@ export const session = sqliteTable( ), active: integer("active", { mode: "boolean" }), data: text("data", { mode: "json" }).notNull(), + wsid: text("wsid", { mode: "json" }).default([]), expired_at: integer("expired_at", { mode: "timestamp_ms" }), }, (table) => { diff --git a/server/session/store/session-store.ts b/session/store/session-store.ts similarity index 58% rename from server/session/store/session-store.ts rename to session/store/session-store.ts index f3bf02e..269f933 100755 --- a/server/session/store/session-store.ts +++ b/session/store/session-store.ts @@ -1,10 +1,10 @@ /// import Database from "bun:sqlite"; -import { and, eq, sql } from "drizzle-orm"; +import { and, ConsoleLogWriter, eq, sql } from "drizzle-orm"; import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite"; import { mkdirSync } from "fs"; import { join } from "path"; -import { dir } from "../../utils/dir"; +import { dir } from "../../server/utils/dir"; import { SessionData, SessionStore, SingleSession } from "../type"; import { session } from "./schema"; @@ -28,6 +28,7 @@ CREATE TABLE IF NOT EXISTS session ( created_at integer NOT NULL DEFAULT current_timestamp, active integer, data text NOT NULL, + wsid text default \`[]\`, expired_at integer ); CREATE INDEX IF NOT EXISTS expired_at_idx ON session (expired_at); @@ -68,24 +69,64 @@ CREATE INDEX IF NOT EXISTS expired_at_idx ON session (expired_at); return { create(data) { - return createSingleStore( + try { + return createSingleStore( + db.session + .insert(session) + .values({ + uid: data.uid, + data, + active: true, + expired_at: data.expired_at + ? new Date(data.expired_at * 1000) + : undefined, + }) + .returning() + .get() + ); + } catch (e) { + console.error("Session Create Error:\n", e); + } + }, + update(where, data) { + try { db.session - .insert(session) - .values({ - uid: data.uid, - data, - active: true, - expired_at: data.expired_at - ? new Date(data.expired_at * 1000) - : undefined, - }) - .returning() - .get() - ); + .update(session) + .set(data) + .where( + and( + ...Object.entries(where || {}).map(([k, v]) => { + return eq(k as any, v); + }) + ) + ) + .get(); + } catch (e) { + console.error("Session Update Error:\n", e); + } }, findFirst(arg) { - return createSingleStore( - db.session + try { + return createSingleStore( + db.session + .select() + .from(session) + .where( + and( + ...Object.entries(arg || {}).map(([k, v]) => { + return eq((session as any)[k], v); + }) + ) + ) + .get() + ); + } catch (e) { + console.error("Session FindFirst Error:\n", e); + } + }, + findMany(arg) { + try { + return db.session .select() .from(session) .where( @@ -95,22 +136,11 @@ CREATE INDEX IF NOT EXISTS expired_at_idx ON session (expired_at); }) ) ) - .get() - ); - }, - findMany(arg) { - return db.session - .select() - .from(session) - .where( - and( - ...Object.entries(arg || {}).map(([k, v]) => { - return eq((session as any)[k], v); - }) - ) - ) - .all() - .map((e) => createSingleStore(e)); + .all() + .map((e) => createSingleStore(e)); + } catch (e) { + console.error("Session FindMany Error:\n", e); + } }, } as SessionStore; }; diff --git a/server/session/type.ts b/session/type.ts similarity index 70% rename from server/session/type.ts rename to session/type.ts index 2980c0f..2cc6c73 100755 --- a/server/session/type.ts +++ b/session/type.ts @@ -32,6 +32,10 @@ export type SessionStore = { data?: T; expired_at?: number; }) => SingleSession; + update: ( + where: Partial, + data: { active?: boolean; wsid?: string[] } + ) => SingleSession[]; findMany: (arg?: Partial) => SingleSession[]; findFirst: (arg?: Partial) => null | SingleSession; }; @@ -40,16 +44,22 @@ export type ServerSession = SessionStore & { current?: SingleSession; }; -export type ClientSessionStatus = "checking" | "guest" | "expired" | "active"; +export type ClientSessionStatus = + | "checking" + | "guest" + | "expired" + | "active" + | "logout"; export type ClientSession = { status: ClientSessionStatus; current: null | SessionData; - connect(): Promise<{ status: ClientSessionStatus }>; - login(arg: { - method: "user-pass"; - username: string; - password: string; - }): Promise; + wsid: string; + connectURL: URL; + ws?: WebSocket; + init(): Promise<{ status: ClientSessionStatus }>; + connect(auth?: SessionAuth): Promise; + connected: boolean; + login(auth: SessionAuth): Promise>; logout(): Promise; }; @@ -66,3 +76,9 @@ export type ServerContext = { pathname: string; }; }; + +export type SessionAuth = { + method: "user-pass"; + username: string; + password: string; +}; diff --git a/utils/client-session.ts b/utils/client-session.ts deleted file mode 100755 index 2a5a8ea..0000000 --- a/utils/client-session.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { SessionData } from "lib/server/session/server-session"; - -const w = window as unknown as { - _prasi_session: any; - _prasi: { site_id: string }; -}; - -type SessionResponse = - | { active: false; reason: string } - | { active: true; data: T; token: string }; - -export const sessionClient = async >(arg: { - editorSampleData: T; - auth: { - mode: "user-pass"; - }; - on?: Partial<{ - active: (arg: { token: string; data: T }) => any; - expired: () => any; - logout: () => any; - broadcast: (arg: { data: any }) => any; - }>; -}): Promise> => { - const session: Session = { - active: false, - id_site: isEditor ? "" : w._prasi.site_id, - async login() {}, - async logout() {}, - token: "", - data: {} as T, - }; - if (isEditor) { - session.active = true; - session.data = arg.editorSampleData; - return session; - } - if (w._prasi_session) return w._prasi_session as Session; - - const s = localStorage.getItem(`prss-${session.id_site}`); - if (!s) { - session.active = false; - } else { - let url = siteurl("/_session"); - if ( - location.hostname === "prasi.avolut.com" || - location.host === "localhost:4550" - ) { - const newurl = new URL(location.href); - newurl.pathname = `/_proxy/${url}`; - url = newurl.toString(); - } - - const prss = await fetch(url, { method: "POST", body: s }); - try { - const resp = (await prss.json()) as SessionResponse; - if (resp) { - if (resp.active) { - session.data = resp.data; - session.active = true; - session.token = resp.token; - } else { - console.warn("Session inactive, reason:" + resp.reason); - } - } - } catch (e) { - console.warn("Failed to activate session"); - } - } - - return session; -}; - -export type Session> = { - active: boolean; - id_site: string; - login: (arg: { username: string; password: string }) => Promise; - logout: () => Promise; - token: string; - data: T; -};