From 65a93711e69474f5d9e1ad8cd1bf4f03a152445d Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Sun, 31 Mar 2024 10:17:22 -0700 Subject: [PATCH 01/14] session basics --- .env.example | 2 + __tests__/actions/session.test.ts | 7 ++- actions/session.ts | 8 ++-- bun.lockb | Bin 65757 -> 69112 bytes config/index.ts | 2 + config/session.ts | 5 +++ initializers/db.ts | 9 ++++ initializers/redis.ts | 37 ++++++++++++++++ initializers/session.ts | 68 ++++++++++++++++++++++++++++++ package.json | 1 + schema/messages.ts | 6 ++- schema/users.ts | 6 ++- 12 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 config/session.ts create mode 100644 initializers/redis.ts create mode 100644 initializers/session.ts diff --git a/.env.example b/.env.example index 3f2af2a..8f21e74 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,8 @@ servers.web.apiRoute="/api" servers.web.assetRoute="/assets" servers.web.pageRoute="/pages" +session.ttl=86400000 + db.connectionString="postgres://postgres:postgres@localhost:5432/bun" db.connectionString.test="postgres://postgres:postgres@localhost:5432/bun-test" db.autoMigrate=true diff --git a/__tests__/actions/session.test.ts b/__tests__/actions/session.test.ts index de8e138..2c93a58 100644 --- a/__tests__/actions/session.test.ts +++ b/__tests__/actions/session.test.ts @@ -33,8 +33,11 @@ test("returns user when matched", async () => { const response = (await res.json()) as ActionResponse; expect(res.status).toBe(200); - expect(response.id).toEqual(1); - expect(response.name).toEqual("Mario Mario"); + expect(response.user.id).toEqual(1); + expect(response.user.name).toEqual("Mario Mario"); + expect(response.session.createdAt).toBeGreaterThan(0); + expect(response.session.id).toEqual(response.user.id); + expect(response.session.csrfToken).not.toBe(null); }); test("fails when users is not found", async () => { diff --git a/actions/session.ts b/actions/session.ts index 64e693e..5a42356 100644 --- a/actions/session.ts +++ b/actions/session.ts @@ -1,5 +1,5 @@ import { eq } from "drizzle-orm"; -import { api, type Action, type ActionParams } from "../api"; +import { api, type Action, type ActionParams, Connection } from "../api"; import { users } from "../schema/users"; import { ensureString } from "../util/formatters"; import { emailValidator, passwordValidator } from "../util/validators"; @@ -23,7 +23,7 @@ export class SessionCreate implements Action { }, }; - run = async (params: ActionParams) => { + run = async (params: ActionParams, connection: Connection) => { const [user] = await api.db.db .select() .from(users) @@ -41,6 +41,8 @@ export class SessionCreate implements Action { ); } - return serializeUser(user); + const session = await api.session.create(connection, user); + + return { user: serializeUser(user), session }; }; } diff --git a/bun.lockb b/bun.lockb index 5305a7975691f6a822710912859e06c21324ee0a..6f3647f94d229e09553d29e906c1fd4d3879fde3 100755 GIT binary patch delta 10602 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 AFaQ7m delta 8772 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 + } +} + +export class Redis extends Initializer { + constructor() { + super(namespace); + this.startPriority = 110; + this.stopPriority = 900; + } + + async initialize() { + const redisContainer = {} as { redis: RedisClient }; + return redisContainer; + } + + async start() { + api.redis.redis = new RedisClient(config.redis.connectionString); + logger.info("redis connection established"); + } + + async stop() { + if (api.redis.redis) { + await api.redis.redis.quit(); // will wait for all pending commands to complete + logger.info("redis connection closed"); + } + } +} diff --git a/initializers/session.ts b/initializers/session.ts new file mode 100644 index 0000000..11e7b69 --- /dev/null +++ b/initializers/session.ts @@ -0,0 +1,68 @@ +import { api, Connection } from "../api"; +import { Initializer } from "../classes/Initializer"; +import { config } from "../config"; +import type { User } from "../schema/users"; + +const namespace = "session"; + +export interface SessionData { + id: number; + csrfToken: string; + createdAt: number; +} + +declare module "../classes/API" { + export interface API { + [namespace]: Awaited>; + } +} + +export class Session extends Initializer { + constructor() { + super(namespace); + this.startPriority = 600; + } + + prefix = `${namespace}:` as const; + + getKey = (connection: Connection) => { + return `${this.prefix}:${connection.id}`; + }; + + load = async (connection: Connection) => { + const key = this.getKey(connection); + const data = await api.redis.redis.get(key); + if (!data) return null; + await api.redis.redis.expire(key, config.session.ttl); + return JSON.parse(data) as SessionData; + }; + + create = async (connection: Connection, user: User) => { + const key = this.getKey(connection); + const csrfToken = crypto.randomUUID() + ":" + crypto.randomUUID(); + + const sessionData: SessionData = { + id: user.id, + csrfToken: csrfToken, + createdAt: new Date().getTime(), + }; + + await api.redis.redis.set(key, JSON.stringify(sessionData)); + await api.redis.redis.expire(key, config.session.ttl); + return sessionData; + }; + + destroy = async (connection: Connection) => { + const key = this.getKey(connection); + const response = await api.redis.redis.del(key); + return response > 0; + }; + + async initialize() { + return { + load: this.load, + create: this.create, + destroy: this.destroy, + }; + } +} diff --git a/package.json b/package.json index fa109da..ba67df3 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "colors": "^1.4.0", "drizzle-orm": "^0.30.4", + "ioredis": "^5.3.2", "pg": "^8.11.3", "react": "^18.2.0", "react-bootstrap": "^2.10.1", diff --git a/schema/messages.ts b/schema/messages.ts index 44c090c..fe1d034 100644 --- a/schema/messages.ts +++ b/schema/messages.ts @@ -1,5 +1,6 @@ import { pgTable, serial, text, integer, timestamp } from "drizzle-orm/pg-core"; import { users } from "./users"; +import { sql } from "drizzle-orm"; export const messages = pgTable("messages", { id: serial("id").primaryKey(), @@ -8,7 +9,10 @@ export const messages = pgTable("messages", { .references(() => users.id) .notNull(), createdAt: timestamp("created_at").notNull().defaultNow(), - updatedAt: timestamp("updated_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at") + .notNull() + .defaultNow() + .$onUpdateFn(() => sql`updated_at = NOW()`), }); export type Message = typeof messages.$inferSelect; diff --git a/schema/users.ts b/schema/users.ts index 380239c..08cc645 100644 --- a/schema/users.ts +++ b/schema/users.ts @@ -1,3 +1,4 @@ +import { sql } from "drizzle-orm"; import { pgTable, serial, @@ -15,7 +16,10 @@ export const users = pgTable( email: text("email").notNull().unique(), password_hash: text("password_hash").notNull(), createdAt: timestamp("created_at").notNull().defaultNow(), - updatedAt: timestamp("updated_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at") + .notNull() + .defaultNow() + .$onUpdateFn(() => sql`updated_at = NOW()`), }, (users) => { return { From 3ab8875975e8adb084b0aa820aa442066e7a08d7 Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Sun, 31 Mar 2024 19:22:55 -0700 Subject: [PATCH 02/14] session setup --- .env.example | 1 + __tests__/actions/session.test.ts | 2 +- actions/session.ts | 16 +++++++++-- classes/Connection.ts | 46 ++++++++++++++++++++++++++----- classes/TypedError.ts | 3 ++ config/session.ts | 1 + initializers/session.ts | 19 ++++++++++--- servers/web.ts | 36 +++++++++++++++++------- 8 files changed, 99 insertions(+), 25 deletions(-) diff --git a/.env.example b/.env.example index 8f21e74..93751f2 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,7 @@ servers.web.assetRoute="/assets" servers.web.pageRoute="/pages" session.ttl=86400000 +session.cookieName="__session" db.connectionString="postgres://postgres:postgres@localhost:5432/bun" db.connectionString.test="postgres://postgres:postgres@localhost:5432/bun-test" diff --git a/__tests__/actions/session.test.ts b/__tests__/actions/session.test.ts index 2c93a58..594af4e 100644 --- a/__tests__/actions/session.test.ts +++ b/__tests__/actions/session.test.ts @@ -36,8 +36,8 @@ test("returns user when matched", async () => { expect(response.user.id).toEqual(1); expect(response.user.name).toEqual("Mario Mario"); expect(response.session.createdAt).toBeGreaterThan(0); - expect(response.session.id).toEqual(response.user.id); expect(response.session.csrfToken).not.toBe(null); + expect(response.session.data.userId).toEqual(response.user.id); }); test("fails when users is not found", async () => { diff --git a/actions/session.ts b/actions/session.ts index 5a42356..aead241 100644 --- a/actions/session.ts +++ b/actions/session.ts @@ -6,6 +6,7 @@ import { emailValidator, passwordValidator } from "../util/validators"; import { serializeUser, checkPassword } from "../ops/UserOps"; import { ErrorType, TypedError } from "../classes/TypedError"; import { HTTP_METHOD } from "../classes/Action"; +import type { SessionData } from "../initializers/session"; export class SessionCreate implements Action { name = "sessionCreate"; @@ -23,7 +24,13 @@ export class SessionCreate implements Action { }, }; - run = async (params: ActionParams, connection: Connection) => { + run = async ( + params: ActionParams, + connection: Connection, + ): Promise<{ + user: ReturnType; + session: SessionData; + }> => { const [user] = await api.db.db .select() .from(users) @@ -41,8 +48,11 @@ export class SessionCreate implements Action { ); } - const session = await api.session.create(connection, user); + await connection.updateSession({ userId: user.id }); - return { user: serializeUser(user), session }; + return { + user: serializeUser(user), + session: connection.session as SessionData, + }; }; } diff --git a/classes/Connection.ts b/classes/Connection.ts index b0713b7..d0984f9 100644 --- a/classes/Connection.ts +++ b/classes/Connection.ts @@ -3,16 +3,22 @@ import { config } from "../config"; import colors from "colors"; import type { Action, ActionParams } from "./Action"; import { ErrorType, TypedError } from "./TypedError"; +import type { SessionData } from "../initializers/session"; export class Connection { type: string; - ipAddress: string; + identifier: string; id: string; + session?: SessionData; - constructor(type: string, ipAddress: string) { - this.id = crypto.randomUUID(); + constructor( + type: string, + identifier: string, + id = crypto.randomUUID() as string, + ) { this.type = type; - this.ipAddress = ipAddress; + this.identifier = identifier; + this.id = id; } /** @@ -39,6 +45,8 @@ export class Connection { ); } + await this.loadSession(); + const formattedParams = await this.formatParams(params, action); response = await action.run(formattedParams, this); } catch (e) { @@ -63,17 +71,41 @@ export class Connection { const duration = new Date().getTime() - reqStartTime; logger.info( - `${messagePrefix} ${actionName} (${duration}ms) ${method.length > 0 ? `[${method}]` : ""} ${this.ipAddress}${url.length > 0 ? `(${url})` : ""} ${error ? error : ""} ${loggingParams}`, + `${messagePrefix} ${actionName} (${duration}ms) ${method.length > 0 ? `[${method}]` : ""} ${this.identifier}${url.length > 0 ? `(${url})` : ""} ${error ? error : ""} ${loggingParams}`, ); return { response, error }; } - findAction(actionName: string | undefined) { + async updateSession(data: Record) { + await this.loadSession(); + + if (!this.session) { + throw new TypedError( + "Session not found", + ErrorType.CONNECTION_SESSION_NOT_FOUND, + ); + } + + return api.session.update(this.session, data); + } + + private async loadSession() { + if (this.session) return; + + const session = await api.session.load(this); + if (session) { + this.session = session; + } else { + this.session = await api.session.create(this); + } + } + + private findAction(actionName: string | undefined) { return api.actions.actions.find((a) => a.name === actionName); } - async formatParams(params: FormData, action: Action) { + private async formatParams(params: FormData, action: Action) { if (!action.inputs) return {} as ActionParams; const formattedParams = {} as ActionParams; diff --git a/classes/TypedError.ts b/classes/TypedError.ts index bb2f0ac..d499bdf 100644 --- a/classes/TypedError.ts +++ b/classes/TypedError.ts @@ -11,6 +11,9 @@ export enum ErrorType { "TASK_VALIDATION" = "TASK_VALIDATION", "SERVER_VALIDATION" = "SERVER_VALIDATION", + // session + "CONNECTION_SESSION_NOT_FOUND" = "CONNECTION_SESSION_NOT_FOUND", + // actions "CONNECTION_SERVER_ERROR" = "CONNECTION_SERVER_ERROR", "CONNECTION_ACTION_NOT_FOUND" = "CONNECTION_ACTION_NOT_FOUND", diff --git a/config/session.ts b/config/session.ts index 0cc6cc3..251bc53 100644 --- a/config/session.ts +++ b/config/session.ts @@ -2,4 +2,5 @@ import { loadFromEnvIfSet } from "../util/config"; export const configSession = { ttl: await loadFromEnvIfSet("session.ttl", 1000 * 60 * 60 * 24), // one day, in seconds + cookieName: await loadFromEnvIfSet("session.cookieName", "__session"), }; diff --git a/initializers/session.ts b/initializers/session.ts index 11e7b69..2e95bb8 100644 --- a/initializers/session.ts +++ b/initializers/session.ts @@ -1,14 +1,15 @@ import { api, Connection } from "../api"; import { Initializer } from "../classes/Initializer"; import { config } from "../config"; -import type { User } from "../schema/users"; const namespace = "session"; export interface SessionData { - id: number; + key: string; + cookieName: typeof config.session.cookieName; csrfToken: string; createdAt: number; + data: Record; } declare module "../classes/API" { @@ -37,14 +38,16 @@ export class Session extends Initializer { return JSON.parse(data) as SessionData; }; - create = async (connection: Connection, user: User) => { + create = async (connection: Connection, data: Record = {}) => { const key = this.getKey(connection); const csrfToken = crypto.randomUUID() + ":" + crypto.randomUUID(); const sessionData: SessionData = { - id: user.id, + key, + cookieName: config.session.cookieName, csrfToken: csrfToken, createdAt: new Date().getTime(), + data, }; await api.redis.redis.set(key, JSON.stringify(sessionData)); @@ -52,6 +55,13 @@ export class Session extends Initializer { return sessionData; }; + update = async (session: SessionData, data: Record) => { + session.data = { ...session.data, ...data }; + await api.redis.redis.set(session.key, JSON.stringify(session)); + await api.redis.redis.expire(session.key, config.session.ttl); + return session.data; + }; + destroy = async (connection: Connection) => { const key = this.getKey(connection); const response = await api.redis.redis.del(key); @@ -62,6 +72,7 @@ export class Session extends Initializer { return { load: this.load, create: this.create, + update: this.update, destroy: this.destroy, }; } diff --git a/servers/web.ts b/servers/web.ts index 9b7d4ee..fa2cb2a 100644 --- a/servers/web.ts +++ b/servers/web.ts @@ -10,9 +10,18 @@ import { ErrorType, TypedError } from "../classes/TypedError"; type URLParsed = import("url").URL; -const commonHeaders = { - "Content-Type": "application/json", - "x-server-name": config.process.name, +const buildHeaders = (connection?: Connection) => { + const headers: Record = {}; + + headers["Content-Type"] = "application/json"; + headers["x-server-name"] = config.process.name; + + if (connection) { + headers["Set-Cookie"] = + `${config.session.cookieName}=${connection.id}; Max-Age=${config.session.ttl}`; //HttpOnly; SameSite=Strict; Path=/ + } + + return headers; }; export class WebServer extends Server> { @@ -71,18 +80,25 @@ export class WebServer extends Server> { } async handleAction(request: Request, url: URLParsed) { - if (!this.server) - throw new TypedError("Serb server not started", ErrorType.SERVER_START); + if (!this.server) { + throw new TypedError("Server server not started", ErrorType.SERVER_START); + } + let errorStatusCode = 500; const ipAddress = this.server.requestIP(request)?.address || "unknown"; - const connection = new Connection(this.name, ipAddress); + const cookies = request.headers.getSetCookie(); + const idFromCookie = cookies + .filter((c) => c.split("=")[0] === config.session.cookieName)[0] + ?.split("=")[1]; + const connection = new Connection(this.name, ipAddress, idFromCookie); const actionName = await this.determineActionName( url, request.method as HTTP_METHOD, ); if (!actionName) errorStatusCode = 404; + // param load order: url params -> body params -> query params let params: FormData; try { params = await request.formData(); @@ -113,7 +129,7 @@ export class WebServer extends Server> { return error ? this.buildError(error, errorStatusCode) - : this.buildResponse(response); + : this.buildResponse(connection, response); } async handleAsset(request: Request, url: URLParsed) { @@ -212,10 +228,10 @@ export class WebServer extends Server> { } } - async buildResponse(response: Object, status = 200) { + async buildResponse(connection: Connection, response: Object, status = 200) { return new Response(JSON.stringify(response, null, 2), { status, - headers: commonHeaders, + headers: buildHeaders(connection), }); } @@ -232,7 +248,7 @@ export class WebServer extends Server> { }) + "\n", { status, - headers: commonHeaders, + headers: buildHeaders(), }, ); } From ad05b14cf7cb01b0fe68f800f2a0ba7cdb49e6aa Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Mon, 1 Apr 2024 20:25:24 -0700 Subject: [PATCH 03/14] session basics --- __tests__/actions/user.test.ts | 104 ++++++++++++++++++++++++--------- actions/session.ts | 4 +- actions/user.ts | 48 ++++++++++++++- bun.lockb | Bin 69112 -> 70964 bytes classes/Connection.ts | 9 ++- initializers/session.ts | 21 +++---- ops/MessageOps.ts | 11 ++++ ops/UserOps.ts | 6 +- package.json | 10 ++-- schema/messages.ts | 3 +- schema/users.ts | 3 +- servers/web.ts | 1 + 12 files changed, 164 insertions(+), 56 deletions(-) create mode 100644 ops/MessageOps.ts diff --git a/__tests__/actions/user.test.ts b/__tests__/actions/user.test.ts index 092930a..dc9866d 100644 --- a/__tests__/actions/user.test.ts +++ b/__tests__/actions/user.test.ts @@ -1,7 +1,8 @@ -import { test, expect, beforeAll, afterAll } from "bun:test"; +import { describe, test, expect, beforeAll, afterAll } from "bun:test"; import { api, type ActionResponse } from "../../api"; -import type { UserCreate } from "../../actions/user"; +import type { UserCreate, UserEdit } from "../../actions/user"; import { config } from "../../config"; +import type { SessionCreate } from "../../actions/session"; const url = `http://${config.server.web.host}:${config.server.web.port}`; @@ -14,34 +15,83 @@ afterAll(async () => { await api.stop(); }); -test("user can be created", async () => { - const res = await fetch(url + "/api/user", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: "person 1", - email: "person1@example.com", - password: "password", - }), +describe("userCreate", () => { + test("user can be created", async () => { + const res = await fetch(url + "/api/user", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: "Mario Mario", + email: "mario@example.com", + password: "mushroom1", + }), + }); + const response = (await res.json()) as ActionResponse; + expect(res.status).toBe(200); + + expect(response.user.id).toEqual(1); + expect(response.user.email).toEqual("mario@example.com"); }); - const response = (await res.json()) as ActionResponse; - expect(res.status).toBe(200); - expect(response.id).toEqual(1); - expect(response.email).toEqual("person1@example.com"); + test("email must be unique", async () => { + const res = await fetch(url + "/api/user", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: "Mario Mario", + email: "mario@example.com", + password: "mushroom1", + }), + }); + const response = (await res.json()) as ActionResponse; + expect(res.status).toBe(500); + expect(response.error?.message).toMatch(/violates unique constraint/); + }); }); -test("email must be unique", async () => { - const res = await fetch(url + "/api/user", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: "person 1", - email: "person1@example.com", - password: "password", - }), +describe("userEdit", () => { + test("it fails without a session", async () => { + const res = await fetch(url + "/api/user", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "new name" }), + }); + const response = (await res.json()) as ActionResponse; + expect(res.status).toBe(500); + expect(response.error?.message).toMatch(/User not found/); + }); + + test("the user can be updated", async () => { + const sessionRes = await fetch(url + "/api/session", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: "mario@example.com", + password: "mushroom1", + }), + }); + const sessionResponse = + (await sessionRes.json()) as ActionResponse; + expect(sessionRes.status).toBe(200); + const csrfToken = sessionResponse.session.csrfToken; + const sessionId = sessionResponse.session.id; + + await Bun.sleep(1001); + + const res = await fetch(url + "/api/user", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Set-Cookie": `${config.session.cookieName}=${sessionId}`, + }, + body: JSON.stringify({ name: "new name" }), + }); + const response = (await res.json()) as ActionResponse; + expect(res.status).toBe(200); + expect(response.user.name).toEqual("new name"); + expect(response.user.email).toEqual("mario@example.com"); + expect(sessionResponse.user.updatedAt).toBeLessThan( + response.user.updatedAt, + ); }); - const response = (await res.json()) as ActionResponse; - expect(res.status).toBe(500); - expect(response.error?.message).toMatch(/violates unique constraint/); }); diff --git a/actions/session.ts b/actions/session.ts index aead241..8f4d729 100644 --- a/actions/session.ts +++ b/actions/session.ts @@ -28,7 +28,7 @@ export class SessionCreate implements Action { params: ActionParams, connection: Connection, ): Promise<{ - user: ReturnType; + user: Awaited>; session: SessionData; }> => { const [user] = await api.db.db @@ -51,7 +51,7 @@ export class SessionCreate implements Action { await connection.updateSession({ userId: user.id }); return { - user: serializeUser(user), + user: await serializeUser(user), session: connection.session as SessionData, }; }; diff --git a/actions/user.ts b/actions/user.ts index fa7c1f4..8c8901c 100644 --- a/actions/user.ts +++ b/actions/user.ts @@ -1,4 +1,5 @@ -import { api, Action, type ActionParams } from "../api"; +import { eq } from "drizzle-orm"; +import { api, Action, type ActionParams, Connection } from "../api"; import { HTTP_METHOD } from "../classes/Action"; import { hashPassword, serializeUser } from "../ops/UserOps"; import { users } from "../schema/users"; @@ -8,6 +9,7 @@ import { nameValidator, passwordValidator, } from "../util/validators"; +import { ErrorType, TypedError } from "../classes/TypedError"; export class UserCreate implements Action { name = "userCreate"; @@ -40,6 +42,48 @@ export class UserCreate implements Action { }) .returning(); - return serializeUser(user); + return { user: await serializeUser(user) }; + } +} + +export class UserEdit implements Action { + name = "userEdit"; + web = { route: "/user", method: HTTP_METHOD.POST }; + inputs = { + name: { + required: false, + validator: nameValidator, + formatter: ensureString, + }, + email: { + required: false, + validator: emailValidator, + formatter: ensureString, + }, + password: { + required: false, + validator: passwordValidator, + formatter: ensureString, + }, + }; + + async run(params: ActionParams, connection: Connection) { + if (!connection?.session?.data.userId) { + throw new TypedError("User not found", ErrorType.CONNECTION_ACTION_RUN); + } + + const { name, email, password } = params; + const updates = {} as Record; + if (name) updates.name = name; + if (email) updates.email = email.toLowerCase(); + if (password) updates.password_hash = await hashPassword(password); + + const [user] = await api.db.db + .update(users) + .set(updates) + .where(eq(users.id, connection.session.data.userId)) + .returning(); + + return { user: await serializeUser(user) }; } } diff --git a/bun.lockb b/bun.lockb index 6f3647f94d229e09553d29e906c1fd4d3879fde3..a510f39f0660228f5f4b527432be6b7833d0aadc 100755 GIT binary patch delta 10318 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 D5yjX7 delta 9498 zcmeHNdw7i3y8qTEA%l=aWM+~QGi{R^3+w zOA-S2d;!YS$SRKOnpIjNVIuBq48(Kd;g*M}tHdkKWej9)$G%)D(VkbSAgf@4dsH&GY=Fi>X`z{yOpWw2WxUK&E8ql8#TQE4TAsmFr{6Y=M)%> z;n@w!h7F+H@iNuHGY1q~=qc9Z(V*-%0F?dWKwE=`Y4&EC{pV11VwXUWX~?%8_S)vOcG?+EFbiuk1&lyh8uh%3yrYeU|1wzA|^P zj1jzdK{0C13W(p}v3iQM!OsVnk}R{$v{HLXc*JWEiDk00%8l2$X!=iNp%rsior zbTc^Gkc0K;14lu1es-~rt^_6-&O;Xqofj1~w;TMiQr#iRiZlz-0L33RahR^OP7-%1 zA}Gnw8@c+hVzK(#4YM^#CP-pGT?tB(?>48%5Qm``CK?aNMr1YGy^A3Yrt7T|4Sxow zPK7)0#*@dA(t+Hs1zj~d#5WXSN-`wk;e1H#0FKi&Nnn26eITT&CX?OJ1WV8tk`jpb zKuEFF)s!fg`%$&oA!_J~Imz%D)OZ8&3YhJNj(8#URwTqH=lWB1utVM&KzSC2@S=5= zB)NMlx@vLAGh0z)8;AUUE6Qu*@V<$Bj-`UYMDH%xmhN=Db)q~YkRsbU47;J=6+^hb zcJHQ0_&z)dLn=64FGX)y1S$I7EOy_s)wQ-8ys+CnRa3Z`-xgv-nLml#a zL6irQX{72X%VpQ2-ro3>6TyCc7aED$}iBpZ)~9fNXB zFkOZ6nx;fMlU?o*LU|8547qp{@*I_6<#i!+^+AXHX$VEOcNjt}k`$>7#Gxlc;?S{3 zZS8Wog|5P76O_FFGHq^Qm%p`8UI&MvAKsQcx}*$L-iEG1xf9BJo`zo_*%X($K)ZJe zUXwlPI<~9=Twl8GmuNTuj(5R_JMs2z2Z>kO5C@JYj2ywrxZBayP7cFsQ1X(f@t$o* zc@Yjnt58XDD4tj+c}OT-jc~{tLMbxR;k_wbl9H)wWTN*E;QA?C;sf|xQn(6ms&)T8 zUBH97-*9lMbtO2p#n<2lDs5xi>$;iX1}VBj;8d%*gWgLTIJNCMaB6>ly{EHx)LT@5 zQ|EUOoKxv9sFU7yEI762_4jl?f>Zi4^oUS5Le1+EO)^kjgk9bkLDkU?!{?f^38$SL z97&N@hx~LTHi!2DD3j^BEz$cpxPA&}#!Iok!i@u`TG!mueFskUvq$OH8Q|0wAA%dGv<>K{ z>qdebr0A-_sn(ytsl7OmM{3(S;MCC$-P5(U>Mh2BQ(LS9=T!Rp0i4>l7xGo@c^)`L zC!erUb$5rM9bWnB<5t(gZpenzM_s5uySz4r@)8{KXE9Wr;P4Me>hjYnB(*f7u;v}< z^X5KsW^7Gfk3OQz0K-tt%9K4H*JP&Nz(|1gqW~`d8_MlQE3F!&Wqgj3vNZ>$+#pAj ze~YqTuIASe#d}IB2V`It!2T5)oej!mHXkL}@HEf}cm`+!ECRSRMA^SmvHoimpJvi4 znmWeFu~a zQ})}Y(H$Dy3Ch!XAK+2;06xHefb|CeE=;-KBN7&jJ30n%`A5nV`WWDbrvNTYS^k6t z6sFw%Qx;H|vV0aW0yh9I_oqtyzvb{hb?|T6wef#rhW8(UBmDv375PzhQz+|y2AJN` z^#4eMplivEP|9@X|A>yfH)kGrVUu?u!AK^gMs=*5w~}mGo-bneW~?Wr%0ysu`U`m#+N>X)Q|krT;wy>mnNq0LceO~b4q4WzJfh%X(p9_JK;s20*bNO2iX{FGRpf%wK@){q_}YbN5$z^pT!VkljN zbQ)4}mQ%Q>G7Ir#`qGb(hEwls#FvFN%XW&9R0ruYq@g)ZF`BA!5MMUp%XNyeG$a@C zPjJkphlU-sqrK5hDwxOOw{!?6HF1b-ZLwiuqBl8n3@hlago=^KxFQBke zmw1k*qFzX~rHJAQL^0JVDye)bq9{cakch0)5XDqPG0iDnpo@@BLrR|R6icacx=XxB zS5Pmb-ZNa{C0d61WvWBHochmni4|0ZdL`XL{R$0v(j{J{b*QUIl)1zz8jkuk+KhTN z89XlWI;Eq2gSMewL;mG1v6kGZ*U=u-)ntCkCElbW)az+K>J1b&%Oy6_RMeZO7WG>c zQQ;Dssk{PF%t90u&VhWO)r(ig9AiKR_TUReCVf@d-=Pk4EJ5Vff=)m)z))0igCZ!VVv z(UbFgcoUAFzErg^2j7mod>@5`FD9NSGtMtHRSQSTtfs+>edJZ4H7gfxmsJ`s*Q|bi zTX@s8+5}U!(5QDGDwBS_s}`Q~3jm*U_=^q~UFOpspMAJ20hr}O_%eXYQh;@Q@E)S^ zFKRNMwOa#RmILh97*IbfI!V$BP03$o_^8Rol>i?lOShmxNqL7H?}bz5w(bEu4?c0s z0p^ z0^m7dArKBc0Pxw7=lVUsXa94+dEf%@8Spu95%>!|vpvbm-!f`}D-hLU+l$1?uz#%Ir;K>5^t8sINZ>ws$DO<+B+26zKl1-uIIx842#pI7-* zeiPUZyaj9q{sjCP*b96Jd;~aw_W<6fa^Qp{*(Dd4VZd-;1keZIz2Kw$ZQyUf9pD$> zSKuyyFL`Hn^k`c84)c<WDM}zG~3w9>? zCW6DS1pGslWT)DlM(>%M7_#{%r9>&Ei00W4%_ zM%}<2=zK2tG=TFg8{k3o0d*gJ(DAV40h53tU?MO9C;$op4oc^FmrCf?u0au{;GO`c z0Fwa%DuKnoQs8-D3Gf0H?RG}J4DKa>-x@Chn}D}~%|H{N7jSb2DlVt#FS}iGcZm}B z#0nct+>;#~E9qBwJ(qrY=~zl=kj$HpLf`EP#v!KJ2Qk80lk~yt#&qLwP>6m3IIK(L z^&JJ@7t6vL6&)1~e;e)D7eon1+K9g7Ix;Y1NK^GgU1LJl_)C%rp90=F1IB;Wtn0zzN{0`cJYEAj(5mRNR(fTcD1^o$*~ zu*qx3+H*^8hvzSVEQeC#AA^H0945Orp6Hj{_eTAoX|VXgGUbn5yxSj{{an3eVgRi? zWQs3?1JAea$1c@RdsW@yzU@li`u*$lYdMGK27D7<-*ia;T|0C+z6wnP(KIF!&*m(VlSvvo6Ou@vA<9N=Lo#M-5_Lj$l z(DoBH`HNukJ7uEACu8O1AvHZuS_E6xLpAB+Sc`srb&sBu)Ny&e!>KSD{HZBkyFT|s26T!M&wf!=?;yg}`|V`Hdg$${#apJ;TQVZ4@Qlf#U%R(%(SJln?k7Ez z46yT)@KGeKI%Biw_wP&mJiTK^Z%kGaD=x}rl|JfB-<>hV=@;`~UfUkLJM6?I*5ej8LYnSO$dA{Ie!YgIS z4h?33VnG(PIA@A)Fbfn1{f{PUtNhyR?Q~^O{j{!!(WrAKOM_XUG({FHLf;K$f#T3$ z7AO{E!38vJFbfn1{f|NR{p4vkWA#v~pVqu^>T=#>X)p_vra>{3cHSfpkD+PjQy={2 z4-xff5EF+}+-ddQwdgdpz0e2e3mS957CbCg{hN)7?s11iX^UTXYhG!-HPsgqno`TU mb~TqTH*QpuQx_>}p1)S;wKO(Rgw(9RksBtgO9uyvFaHi{h|H4! diff --git a/classes/Connection.ts b/classes/Connection.ts index d0984f9..4252b1e 100644 --- a/classes/Connection.ts +++ b/classes/Connection.ts @@ -127,7 +127,10 @@ export class Connection { ); } - if ((paramDefinition.required && value === undefined) || value === null) { + if ( + paramDefinition.required === true && + (value === undefined || value === null) + ) { throw new TypedError( `Missing required param: ${key}`, ErrorType.CONNECTION_ACTION_PARAM_REQUIRED, @@ -135,7 +138,7 @@ export class Connection { ); } - if (paramDefinition.formatter) { + if (paramDefinition.formatter && value !== undefined && value !== null) { try { value = paramDefinition.formatter(value); } catch (e) { @@ -148,7 +151,7 @@ export class Connection { } } - if (paramDefinition.validator) { + if (paramDefinition.validator && value !== undefined && value !== null) { const validationResponse = paramDefinition.validator(value); if (validationResponse !== true) { throw new TypedError( diff --git a/initializers/session.ts b/initializers/session.ts index 2e95bb8..34ec24f 100644 --- a/initializers/session.ts +++ b/initializers/session.ts @@ -5,7 +5,7 @@ import { config } from "../config"; const namespace = "session"; export interface SessionData { - key: string; + id: string; cookieName: typeof config.session.cookieName; csrfToken: string; createdAt: number; @@ -24,14 +24,14 @@ export class Session extends Initializer { this.startPriority = 600; } - prefix = `${namespace}:` as const; + prefix = `${namespace}` as const; - getKey = (connection: Connection) => { - return `${this.prefix}:${connection.id}`; + getKey = (connectionId: Connection["id"]) => { + return `${this.prefix}:${connectionId}`; }; load = async (connection: Connection) => { - const key = this.getKey(connection); + const key = this.getKey(connection.id); const data = await api.redis.redis.get(key); if (!data) return null; await api.redis.redis.expire(key, config.session.ttl); @@ -39,11 +39,11 @@ export class Session extends Initializer { }; create = async (connection: Connection, data: Record = {}) => { - const key = this.getKey(connection); + const key = this.getKey(connection.id); const csrfToken = crypto.randomUUID() + ":" + crypto.randomUUID(); const sessionData: SessionData = { - key, + id: connection.id, cookieName: config.session.cookieName, csrfToken: csrfToken, createdAt: new Date().getTime(), @@ -56,14 +56,15 @@ export class Session extends Initializer { }; update = async (session: SessionData, data: Record) => { + const key = this.getKey(session.id); session.data = { ...session.data, ...data }; - await api.redis.redis.set(session.key, JSON.stringify(session)); - await api.redis.redis.expire(session.key, config.session.ttl); + await api.redis.redis.set(key, JSON.stringify(session)); + await api.redis.redis.expire(key, config.session.ttl); return session.data; }; destroy = async (connection: Connection) => { - const key = this.getKey(connection); + const key = this.getKey(connection.id); const response = await api.redis.redis.del(key); return response > 0; }; diff --git a/ops/MessageOps.ts b/ops/MessageOps.ts new file mode 100644 index 0000000..7d20746 --- /dev/null +++ b/ops/MessageOps.ts @@ -0,0 +1,11 @@ +import type { Message } from "../schema/messages"; + +export async function serializeMessage(message: Message) { + return { + id: message.id, + body: message.body, + user_id: message.user_id, + createdAt: message.createdAt.getTime(), + updatedAt: message.updatedAt.getTime(), + }; +} diff --git a/ops/UserOps.ts b/ops/UserOps.ts index 77c333d..2860087 100644 --- a/ops/UserOps.ts +++ b/ops/UserOps.ts @@ -10,12 +10,12 @@ export async function checkPassword(user: User, password: string) { return isMatch; } -export function serializeUser(user: User) { +export async function serializeUser(user: User) { return { id: user.id, name: user.name, email: user.email, - createdAt: user.createdAt, - updatedAt: user.updatedAt, + createdAt: user.createdAt.getTime(), + updatedAt: user.updatedAt.getTime(), }; } diff --git a/package.json b/package.json index ba67df3..5632d06 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "type": "module", "license": "MIT", "devDependencies": { - "@types/bun": "^1.0.8", + "@types/bun": "^1.0.12", "@types/pg": "^8.11.4", - "@types/react-dom": "^18.2.22", + "@types/react-dom": "^18.2.23", "drizzle-kit": "^0.20.14", "prettier": "^3.2.5" }, @@ -16,11 +16,11 @@ }, "dependencies": { "colors": "^1.4.0", - "drizzle-orm": "^0.30.4", + "drizzle-orm": "^0.30.6", "ioredis": "^5.3.2", - "pg": "^8.11.3", + "pg": "^8.11.4", "react": "^18.2.0", - "react-bootstrap": "^2.10.1", + "react-bootstrap": "^2.10.2", "react-dom": "^18.2.0" }, "scripts": { diff --git a/schema/messages.ts b/schema/messages.ts index fe1d034..ac2cf97 100644 --- a/schema/messages.ts +++ b/schema/messages.ts @@ -1,6 +1,5 @@ import { pgTable, serial, text, integer, timestamp } from "drizzle-orm/pg-core"; import { users } from "./users"; -import { sql } from "drizzle-orm"; export const messages = pgTable("messages", { id: serial("id").primaryKey(), @@ -12,7 +11,7 @@ export const messages = pgTable("messages", { updatedAt: timestamp("updated_at") .notNull() .defaultNow() - .$onUpdateFn(() => sql`updated_at = NOW()`), + .$onUpdateFn(() => new Date()), }); export type Message = typeof messages.$inferSelect; diff --git a/schema/users.ts b/schema/users.ts index 08cc645..2a527c8 100644 --- a/schema/users.ts +++ b/schema/users.ts @@ -1,4 +1,3 @@ -import { sql } from "drizzle-orm"; import { pgTable, serial, @@ -19,7 +18,7 @@ export const users = pgTable( updatedAt: timestamp("updated_at") .notNull() .defaultNow() - .$onUpdateFn(() => sql`updated_at = NOW()`), + .$onUpdateFn(() => new Date()), }, (users) => { return { diff --git a/servers/web.ts b/servers/web.ts index fa2cb2a..97b5834 100644 --- a/servers/web.ts +++ b/servers/web.ts @@ -91,6 +91,7 @@ export class WebServer extends Server> { const idFromCookie = cookies .filter((c) => c.split("=")[0] === config.session.cookieName)[0] ?.split("=")[1]; + const connection = new Connection(this.name, ipAddress, idFromCookie); const actionName = await this.determineActionName( url, From 8f8f7631e728860515bcb70599edfe8be2b1503b Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Mon, 1 Apr 2024 20:26:56 -0700 Subject: [PATCH 04/14] remove csrf --- __tests__/actions/session.test.ts | 1 - __tests__/actions/user.test.ts | 1 - initializers/session.ts | 3 --- 3 files changed, 5 deletions(-) diff --git a/__tests__/actions/session.test.ts b/__tests__/actions/session.test.ts index 594af4e..cd85a46 100644 --- a/__tests__/actions/session.test.ts +++ b/__tests__/actions/session.test.ts @@ -36,7 +36,6 @@ test("returns user when matched", async () => { expect(response.user.id).toEqual(1); expect(response.user.name).toEqual("Mario Mario"); expect(response.session.createdAt).toBeGreaterThan(0); - expect(response.session.csrfToken).not.toBe(null); expect(response.session.data.userId).toEqual(response.user.id); }); diff --git a/__tests__/actions/user.test.ts b/__tests__/actions/user.test.ts index dc9866d..57ca234 100644 --- a/__tests__/actions/user.test.ts +++ b/__tests__/actions/user.test.ts @@ -73,7 +73,6 @@ describe("userEdit", () => { const sessionResponse = (await sessionRes.json()) as ActionResponse; expect(sessionRes.status).toBe(200); - const csrfToken = sessionResponse.session.csrfToken; const sessionId = sessionResponse.session.id; await Bun.sleep(1001); diff --git a/initializers/session.ts b/initializers/session.ts index 34ec24f..eb72bf4 100644 --- a/initializers/session.ts +++ b/initializers/session.ts @@ -7,7 +7,6 @@ const namespace = "session"; export interface SessionData { id: string; cookieName: typeof config.session.cookieName; - csrfToken: string; createdAt: number; data: Record; } @@ -40,12 +39,10 @@ export class Session extends Initializer { create = async (connection: Connection, data: Record = {}) => { const key = this.getKey(connection.id); - const csrfToken = crypto.randomUUID() + ":" + crypto.randomUUID(); const sessionData: SessionData = { id: connection.id, cookieName: config.session.cookieName, - csrfToken: csrfToken, createdAt: new Date().getTime(), data, }; From 0204a84d2984e3e2562f863d2fdd6f7c713693db Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Mon, 1 Apr 2024 20:32:59 -0700 Subject: [PATCH 05/14] type packages are needed in prod? --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 5632d06..4406b84 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,6 @@ "type": "module", "license": "MIT", "devDependencies": { - "@types/bun": "^1.0.12", - "@types/pg": "^8.11.4", - "@types/react-dom": "^18.2.23", "drizzle-kit": "^0.20.14", "prettier": "^3.2.5" }, @@ -15,6 +12,9 @@ "typescript": "^5.0.0" }, "dependencies": { + "@types/bun": "^1.0.12", + "@types/pg": "^8.11.4", + "@types/react-dom": "^18.2.23", "colors": "^1.4.0", "drizzle-orm": "^0.30.6", "ioredis": "^5.3.2", From ee3a36b471dca9be12b6d12bf6c0c5613a10d260 Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Mon, 1 Apr 2024 20:34:21 -0700 Subject: [PATCH 06/14] rebuild lock --- bun.lockb | Bin 70964 -> 68009 bytes package.json | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lockb b/bun.lockb index a510f39f0660228f5f4b527432be6b7833d0aadc..aa578bc96bb7730dbd1bd51f90eedaf8c0ee7a65 100755 GIT binary patch delta 7944 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 Y)5OcY{&M^q!91=r&S!0#J%9Os07RBvA^-pY diff --git a/package.json b/package.json index 4406b84..5632d06 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "type": "module", "license": "MIT", "devDependencies": { + "@types/bun": "^1.0.12", + "@types/pg": "^8.11.4", + "@types/react-dom": "^18.2.23", "drizzle-kit": "^0.20.14", "prettier": "^3.2.5" }, @@ -12,9 +15,6 @@ "typescript": "^5.0.0" }, "dependencies": { - "@types/bun": "^1.0.12", - "@types/pg": "^8.11.4", - "@types/react-dom": "^18.2.23", "colors": "^1.4.0", "drizzle-orm": "^0.30.6", "ioredis": "^5.3.2", From d76d22eebbfd7d19e4353190b19333f79b9005c8 Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Mon, 1 Apr 2024 20:48:48 -0700 Subject: [PATCH 07/14] use react-dom/server.browser.js --- servers/web.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/servers/web.ts b/servers/web.ts index 97b5834..64107d6 100644 --- a/servers/web.ts +++ b/servers/web.ts @@ -4,10 +4,12 @@ import { logger, api } from "../api"; import { Connection } from "../classes/Connection"; import path from "path"; import { type HTTP_METHOD } from "../classes/Action"; -import { renderToReadableStream } from "react-dom/server"; import type { BunFile } from "bun"; import { ErrorType, TypedError } from "../classes/TypedError"; +// @ts-ignore TODO: Hack because react-dom wants to load the node package, but wa want the browser package for some reason +import { renderToReadableStream } from "react-dom/server.browser.js"; + type URLParsed = import("url").URL; const buildHeaders = (connection?: Connection) => { From 8809e8b4bd423afae3d1186ea99c2cdf7bb61393 Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Mon, 1 Apr 2024 20:51:52 -0700 Subject: [PATCH 08/14] type hack --- servers/web.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/servers/web.ts b/servers/web.ts index 64107d6..99de06d 100644 --- a/servers/web.ts +++ b/servers/web.ts @@ -7,8 +7,14 @@ import { type HTTP_METHOD } from "../classes/Action"; import type { BunFile } from "bun"; import { ErrorType, TypedError } from "../classes/TypedError"; -// @ts-ignore TODO: Hack because react-dom wants to load the node package, but wa want the browser package for some reason -import { renderToReadableStream } from "react-dom/server.browser.js"; +// TODO: Hack because react-dom wants to load the node package, but wa want the browser package for some reason +import ReactDOM from "react-dom/server"; +// @ts-ignore +import reactDomBrowser from "react-dom/server.browser.js"; +const { + renderToReadableStream, +}: { renderToReadableStream: typeof ReactDOM.renderToReadableStream } = + reactDomBrowser; type URLParsed = import("url").URL; From f1a177dba10b5f603882af473eef7fc60f3bc842 Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Mon, 1 Apr 2024 20:54:24 -0700 Subject: [PATCH 09/14] redis host change --- .github/workflows/test.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c52e624..2d2396e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -49,6 +49,8 @@ jobs: - run: bun install - run: cp .env.example .env - run: bun test + env: + redis.connectionString.test: "redis://redis:6379/1" complete: runs-on: ubuntu-latest From b91d2c84317bf9147f73cc238bcb51f315052795 Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Mon, 1 Apr 2024 20:57:19 -0700 Subject: [PATCH 10/14] try secure --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2d2396e..fed195e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -50,7 +50,7 @@ jobs: - run: cp .env.example .env - run: bun test env: - redis.connectionString.test: "redis://redis:6379/1" + redis.connectionString.test: "rediss://redis:6379/1" complete: runs-on: ubuntu-latest From 442eb578f9190d2eafed17ee8376158385e2abe0 Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Mon, 1 Apr 2024 20:57:53 -0700 Subject: [PATCH 11/14] redis pass ports --- .github/workflows/test.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fed195e..c84e593 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -30,6 +30,8 @@ jobs: services: redis: image: redis + ports: + - 6379:6379 postgres: image: postgres env: @@ -50,7 +52,7 @@ jobs: - run: cp .env.example .env - run: bun test env: - redis.connectionString.test: "rediss://redis:6379/1" + redis.connectionString.test: "redis://redis:6379/1" complete: runs-on: ubuntu-latest From 8ff74bf08e73bc9af5a54d2479d3051f0b51cfcf Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Mon, 1 Apr 2024 20:58:52 -0700 Subject: [PATCH 12/14] no override redis --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c84e593..87ae92d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -52,7 +52,7 @@ jobs: - run: cp .env.example .env - run: bun test env: - redis.connectionString.test: "redis://redis:6379/1" + # redis.connectionString.test: "redis://redis:6379/1" complete: runs-on: ubuntu-latest From 7868a992aea05a83fcb5f3e810d04746726ae8ae Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Mon, 1 Apr 2024 20:59:02 -0700 Subject: [PATCH 13/14] fix --- .github/workflows/test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 87ae92d..9651656 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -51,8 +51,8 @@ jobs: - run: bun install - run: cp .env.example .env - run: bun test - env: - # redis.connectionString.test: "redis://redis:6379/1" + # env: + # redis.connectionString.test: "redis://redis:6379/1" complete: runs-on: ubuntu-latest From 17852dfbe7f2c3075e2d7d9421d346c617e9b790 Mon Sep 17 00:00:00 2001 From: Evan Tahler Date: Mon, 1 Apr 2024 21:00:03 -0700 Subject: [PATCH 14/14] cleanup --- .github/workflows/test.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9651656..c8cc9dc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -51,8 +51,6 @@ jobs: - run: bun install - run: cp .env.example .env - run: bun test - # env: - # redis.connectionString.test: "redis://redis:6379/1" complete: runs-on: ubuntu-latest