From 37a94ef2b37d210d95ebb1918a9bce59884e96bc Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 11 Apr 2026 13:44:30 +0200 Subject: [PATCH] refactor(api): remove all Stream Chat dependencies and operations Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/package.json | 1 - .../__tests__/config/stripe-config.test.ts | 2 - .../__tests__/middlewares/middlewares.test.ts | 26 --- apps/api/src/__tests__/routes/invite.test.ts | 40 ---- apps/api/src/__tests__/routes/tablo.test.ts | 60 +----- apps/api/src/__tests__/routes/tasks.test.ts | 28 +-- apps/api/src/__tests__/routes/user.test.ts | 94 +--------- apps/api/src/config.ts | 9 - apps/api/src/helpers/helpers.ts | 8 - apps/api/src/middlewares/middleware.ts | 18 -- apps/api/src/routers/index.ts | 1 - apps/api/src/routers/invite.ts | 30 --- apps/api/src/routers/tablo.ts | 177 +----------------- apps/api/src/routers/tasks.ts | 13 -- apps/api/src/routers/user.ts | 51 +---- apps/api/src/secrets.ts | 4 - apps/api/src/types/app.types.ts | 2 - 17 files changed, 10 insertions(+), 554 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 31fe087..5c18feb 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -33,7 +33,6 @@ "multer": "^2.0.2", "nodemailer": "^7.0.4", "sharp": "^0.34.5", - "stream-chat": "^9.8.0", "stripe": "^20.0.0", "ts-node": "^10.9.2" }, diff --git a/apps/api/src/__tests__/config/stripe-config.test.ts b/apps/api/src/__tests__/config/stripe-config.test.ts index e8f9ca6..fdc130b 100644 --- a/apps/api/src/__tests__/config/stripe-config.test.ts +++ b/apps/api/src/__tests__/config/stripe-config.test.ts @@ -10,10 +10,8 @@ const baseSecrets: Secrets = { emailRefreshToken: "email-refresh-token", r2AccessKeyId: "r2-access-key-id", r2SecretAccessKey: "r2-secret-access-key", - streamChatApiSecret: "stream-chat-api-secret", stripeSecretKey: "sk_live_secret_manager", stripeWebhookSecret: "whsec_live_secret_manager", - streamChatApiSecretStaging: "stream-chat-api-secret-staging", stripeSecretKeyStaging: "sk_live_staging_secret_manager", stripeWebhookSecretStaging: "whsec_live_staging_secret_manager", }; diff --git a/apps/api/src/__tests__/middlewares/middlewares.test.ts b/apps/api/src/__tests__/middlewares/middlewares.test.ts index c18bf7e..489506f 100644 --- a/apps/api/src/__tests__/middlewares/middlewares.test.ts +++ b/apps/api/src/__tests__/middlewares/middlewares.test.ts @@ -427,26 +427,6 @@ describe("Middleware Tests", () => { }); }); - describe("StreamChat Middleware", () => { - it("should inject StreamChat client into context", async () => { - const app = new Hono(); - app.use(middlewareManager.streamChat); - app.get("/test", (c) => { - const streamClient = // biome-ignore lint/suspicious/noExplicitAny: Needed for context access in tests - (c as any).get("streamServerClient"); - return c.json({ hasStreamClient: !!streamClient }); - }); - - // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access - const client = testClient(app) as any; - const res = await client.test.$get(); - const data = await res.json(); - - expect(res.status).toBe(200); - expect(data.hasStreamClient).toBe(true); - }); - }); - describe("R2 Middleware", () => { it("should inject S3 client into context", async () => { const app = new Hono(); @@ -531,18 +511,14 @@ describe("Middleware Tests", () => { it("should chain multiple middlewares correctly", async () => { const app = new Hono(); app.use(middlewareManager.supabase); - app.use(middlewareManager.streamChat); app.use(middlewareManager.stripe); app.get("/test", (c) => { const supabase = // biome-ignore lint/suspicious/noExplicitAny: Needed for context access in tests (c as any).get("supabase"); - const streamClient = // biome-ignore lint/suspicious/noExplicitAny: Needed for context access in tests - (c as any).get("streamServerClient"); const stripe = // biome-ignore lint/suspicious/noExplicitAny: Needed for context access in tests (c as any).get("stripe"); return c.json({ hasSupabase: !!supabase, - hasStreamClient: !!streamClient, hasStripe: !!stripe, }); }); @@ -554,7 +530,6 @@ describe("Middleware Tests", () => { expect(res.status).toBe(200); expect(data.hasSupabase).toBe(true); - expect(data.hasStreamClient).toBe(true); expect(data.hasStripe).toBe(true); }); @@ -562,7 +537,6 @@ describe("Middleware Tests", () => { const app = new Hono(); app.use(middlewareManager.supabase); app.use(middlewareManager.auth); // This will fail - app.use(middlewareManager.streamChat); // This should not execute app.get("/test", (c) => c.json({ success: true })); // biome-ignore lint/suspicious/noExplicitAny: testClient requires any for dynamic route access diff --git a/apps/api/src/__tests__/routes/invite.test.ts b/apps/api/src/__tests__/routes/invite.test.ts index 8fd7f5f..4f8fb8c 100644 --- a/apps/api/src/__tests__/routes/invite.test.ts +++ b/apps/api/src/__tests__/routes/invite.test.ts @@ -1,31 +1,11 @@ import { createClient } from "@supabase/supabase-js"; import { testClient } from "hono/testing"; -import type { Channel, StreamChat } from "stream-chat"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createConfig } from "../../config.js"; import { MiddlewareManager } from "../../middlewares/middleware.js"; import { getMainRouter } from "../../routers/index.js"; import { getTestUser } from "../helpers/dbSetup.js"; -// Mock the stream-chat module -vi.mock("stream-chat", () => { - const mockChannel = { - create: vi.fn().mockResolvedValue(undefined), - sendMessage: vi.fn().mockResolvedValue(undefined), - }; - - const mockStreamChatInstance = { - channel: vi.fn(() => mockChannel), - upsertUser: vi.fn().mockResolvedValue({ users: {} }), - }; - - return { - StreamChat: { - getInstance: vi.fn(() => mockStreamChatInstance), - }, - }; -}); - // Mock nodemailer const mockSendMail = vi.fn(); vi.mock("nodemailer", () => ({ @@ -54,16 +34,7 @@ describe("Booking Endpoint", () => { const createdTablos: string[] = []; const createdUsers: string[] = []; - // Get references to the mocked functions for assertions - let mockStreamChat: StreamChat; - let mockChannel: Channel; - beforeAll(async () => { - // Get references to the mocked instances - const { StreamChat } = await import("stream-chat"); - mockStreamChat = StreamChat.getInstance("test_api_key", "test_api_secret"); - mockChannel = mockStreamChat.channel("messaging", "test_channel_id"); - // Get owner's short_user_id const { data: ownerProfile } = await supabase .from("profiles") @@ -324,10 +295,6 @@ describe("Booking Endpoint", () => { createdUsers.push(userProfile.id); } - // Verify Stream Chat channel was created - expect(mockChannel.create).toHaveBeenCalledTimes(1); - expect(mockChannel.sendMessage).toHaveBeenCalledTimes(1); - // Verify emails were sent (3 emails: welcome to new user, one to owner, one to booker) expect(mockSendMail).toHaveBeenCalledTimes(3); @@ -407,10 +374,6 @@ describe("Booking Endpoint", () => { createdTablos.push(data.tablo_id); createdBookings.push(data.tablo_id); - // Verify Stream Chat channel was created - expect(mockChannel.create).toHaveBeenCalledTimes(1); - expect(mockChannel.sendMessage).toHaveBeenCalledTimes(1); - // Verify emails were sent (2 emails: one to owner, one to booker) expect(mockSendMail).toHaveBeenCalledTimes(2); @@ -511,9 +474,6 @@ describe("Booking Endpoint", () => { expect(data2.tablo_id).toBe(firstTabloId); expect(data2.hasCreatedAccount).toBe(false); - // Stream Chat channel should still be created for the second booking - expect(mockChannel.create).toHaveBeenCalledTimes(1); - // Verify emails were sent for second booking (2 emails) expect(mockSendMail).toHaveBeenCalledTimes(2); diff --git a/apps/api/src/__tests__/routes/tablo.test.ts b/apps/api/src/__tests__/routes/tablo.test.ts index f8c7c7b..b445c90 100644 --- a/apps/api/src/__tests__/routes/tablo.test.ts +++ b/apps/api/src/__tests__/routes/tablo.test.ts @@ -8,35 +8,6 @@ import { getMainRouter } from "../../routers/index.js"; import type { TestUserData } from "../helpers/dbSetup.js"; import { getTestUser } from "../helpers/dbSetup.js"; -// Mock Stream Chat operations -const mockChannelCreate = vi.fn(); -const mockChannelUpdate = vi.fn(); -const mockChannelDelete = vi.fn(); -const mockChannelRemoveMembers = vi.fn(); -const mockChannelAddMembers = vi.fn(); - -// Mock the channel method to return our mocked channel -const mockChannel = { - create: mockChannelCreate, - update: mockChannelUpdate, - delete: mockChannelDelete, - removeMembers: mockChannelRemoveMembers, - addMembers: mockChannelAddMembers, -}; - -// Mock the stream-chat module -vi.mock("stream-chat", () => { - const mockStreamChatInstance = { - channel: vi.fn(() => mockChannel), - }; - - return { - StreamChat: { - getInstance: vi.fn(() => mockStreamChatInstance), - }, - }; -}); - // Mock nodemailer for email sending const mockSendMail = vi.fn(); vi.mock("nodemailer", () => ({ @@ -67,11 +38,6 @@ describe("Tablo Endpoint", () => { beforeEach(() => { // Reset all mocks before each test vi.clearAllMocks(); - mockChannelCreate.mockResolvedValue(undefined); - mockChannelUpdate.mockResolvedValue(undefined); - mockChannelDelete.mockResolvedValue(undefined); - mockChannelRemoveMembers.mockResolvedValue(undefined); - mockChannelAddMembers.mockResolvedValue(undefined); mockSendMail.mockResolvedValue({ messageId: "test-message-id" }); }); @@ -195,7 +161,7 @@ describe("Tablo Endpoint", () => { await supabaseAdmin.from("profiles").update({ plan: "standard" }).eq("id", ownerUser.userId); }); - it("should allow owner to create a tablo and create a Stream Chat channel", async () => { + it("should allow owner to create a tablo", async () => { const res = await createTabloRequest(ownerUser, client, { name: "New Owner Tablo", status: "todo", @@ -205,11 +171,6 @@ describe("Tablo Endpoint", () => { expect(res.status).toBe(200); const data = await res.json(); expect(data.message).toBe("Tablo created successfully"); - - // Verify Stream Chat channel was created - expect(mockChannelCreate).toHaveBeenCalledTimes(1); - // Verify it was called (the channel is created with tablo data) - expect(mockChannelCreate).toHaveBeenCalled(); }); it("should deny temp user from creating a tablo (regularUserCheck blocks temporary users)", async () => { @@ -323,7 +284,6 @@ describe("Tablo Endpoint", () => { expect(res.status).toBe(403); const data = await res.json(); expect(data.error).toBe("You have reached your tablo limit"); - expect(mockChannelCreate).not.toHaveBeenCalled(); } finally { await supabaseAdmin .from("profiles") @@ -392,17 +352,13 @@ describe("Tablo Endpoint", () => { }); describe("DELETE /tablos/delete - Delete Tablo", () => { - it("should allow owner with admin access to delete tablo and delete Stream Chat channel", async () => { + it("should allow owner with admin access to delete tablo", async () => { // Owner has admin access to their tablos const res = await deleteTabloRequest(ownerUser, client, "test_tablo_owner_private"); expect(res.status).toBe(200); const data = await res.json(); expect(data.message).toBe("Tablo deleted successfully"); - - // Verify Stream Chat channel was deleted - expect(mockChannelDelete).toHaveBeenCalledTimes(1); - expect(mockChannelDelete).toHaveBeenCalled(); }); it("should deny temp user without admin access from deleting tablo", async () => { @@ -558,7 +514,7 @@ describe("Tablo Endpoint", () => { return tabloId; }; - it("should allow temp user to leave a shared tablo and remove from Stream Chat channel", async () => { + it("should allow temp user to leave a shared tablo", async () => { const tabloId = await createSharedTabloForLeaveTest({ ownerId: ownerUser.userId, memberId: temporaryUser.userId, @@ -569,13 +525,9 @@ describe("Tablo Endpoint", () => { expect(res.status).toBe(200); const data = await res.json(); expect(data.message).toBe("Tablo left successfully"); - - // Verify Stream Chat channel removeMembers was called - expect(mockChannelRemoveMembers).toHaveBeenCalledTimes(1); - expect(mockChannelRemoveMembers).toHaveBeenCalledWith([temporaryUser.userId]); }); - it("should allow owner to leave a tablo and remove from Stream Chat channel", async () => { + it("should allow owner to leave a tablo", async () => { const tabloId = await createSharedTabloForLeaveTest({ ownerId: temporaryUser.userId, memberId: ownerUser.userId, @@ -587,10 +539,6 @@ describe("Tablo Endpoint", () => { expect(res.status).toBe(200); const data = await res.json(); expect(data.message).toBe("Tablo left successfully"); - - // Verify Stream Chat channel removeMembers was called - expect(mockChannelRemoveMembers).toHaveBeenCalledTimes(1); - expect(mockChannelRemoveMembers).toHaveBeenCalledWith([ownerUser.userId]); }); it("should deny unauthenticated leave request", async () => { diff --git a/apps/api/src/__tests__/routes/tasks.test.ts b/apps/api/src/__tests__/routes/tasks.test.ts index e6d320e..fc1bbd3 100644 --- a/apps/api/src/__tests__/routes/tasks.test.ts +++ b/apps/api/src/__tests__/routes/tasks.test.ts @@ -6,27 +6,6 @@ import { createConfig } from "../../config.js"; import { MiddlewareManager } from "../../middlewares/middleware.js"; import { getMainRouter } from "../../routers/index.js"; -// Mock Stream Chat operations -const mockChannelUpdate = vi.fn(); - -// Mock the channel method to return our mocked channel -const mockChannel = { - update: mockChannelUpdate, -}; - -// Mock the stream-chat module -vi.mock("stream-chat", () => { - const mockStreamChatInstance = { - channel: vi.fn(() => mockChannel), - }; - - return { - StreamChat: { - getInstance: vi.fn(() => mockStreamChatInstance), - }, - }; -}); - // Create S3 mock for calendar file operations const s3Mock = mockClient(S3Client); @@ -45,9 +24,6 @@ describe("Tasks Endpoint", () => { // Mock PutObjectCommand for calendar file writes s3Mock.on(PutObjectCommand).resolves({}); - - // Mock Stream Chat channel update - mockChannelUpdate.mockResolvedValue(undefined); }); describe("POST /tasks/sync-calendars - Sync Calendar Files", () => { @@ -107,8 +83,8 @@ describe("Tasks Endpoint", () => { }); }); - describe("POST /tasks/sync-tablo-names - Sync Tablo Names to Stream", () => { - it("should call sync tablo names endpoint with basic auth and update Stream Chat channels (returns 200 if TASKS_SECRET properly configured)", async () => { + describe("POST /tasks/sync-tablo-names - Sync Tablo Names", () => { + it("should call sync tablo names endpoint with basic auth (returns 200 if TASKS_SECRET properly configured)", async () => { const res = await client.tasks["sync-tablo-names"].$post( {}, { diff --git a/apps/api/src/__tests__/routes/user.test.ts b/apps/api/src/__tests__/routes/user.test.ts index 0567f9e..753c550 100644 --- a/apps/api/src/__tests__/routes/user.test.ts +++ b/apps/api/src/__tests__/routes/user.test.ts @@ -12,25 +12,6 @@ import { MiddlewareManager } from "../../middlewares/middleware.js"; import { getMainRouter } from "../../routers/index.js"; import { getTestUser } from "../helpers/dbSetup.js"; -// Mock Stream Chat operations -const mockUpsertUser = vi.fn(); -const mockCreateToken = vi.fn(); - -// Create an instance object that holds the mocks (like the working pattern in tablo.test.ts) -const mockStreamChatInstanceMethods = { - upsertUser: mockUpsertUser, - createToken: mockCreateToken, -}; - -// Mock the stream-chat module -vi.mock("stream-chat", () => { - return { - StreamChat: { - getInstance: vi.fn(() => mockStreamChatInstanceMethods), - }, - }; -}); - // Create S3 mock for avatar operations const s3Mock = mockClient(S3Client); @@ -50,10 +31,6 @@ describe("User Endpoint", () => { vi.clearAllMocks(); s3Mock.reset(); - // Mock Stream Chat operations - mockUpsertUser.mockResolvedValue({ users: { [ownerUser.userId]: {} } }); - mockCreateToken.mockReturnValue("mock-stream-token-123"); - // Mock S3 operations s3Mock.on(PutObjectCommand).resolves({}); s3Mock.on(ListObjectsV2Command).resolves({ @@ -63,7 +40,7 @@ describe("User Endpoint", () => { }); describe("GET /me - Get User Profile", () => { - it("should return owner user profile with stream token", async () => { + it("should return owner user profile", async () => { const res = await client.users.me.$get( {}, { @@ -78,14 +55,9 @@ describe("User Endpoint", () => { const data = await res.json(); expect(data.id).toBe(ownerUser.userId); expect(data.email).toBe(ownerUser.email); - expect(data.streamToken).toBe("mock-stream-token-123"); - - // Verify Stream Chat createToken was called - expect(mockCreateToken).toHaveBeenCalledTimes(1); - expect(mockCreateToken).toHaveBeenCalledWith(ownerUser.userId); }); - it("should return temp user profile with stream token", async () => { + it("should return temp user profile", async () => { const res = await client.users.me.$get( {}, { @@ -100,11 +72,6 @@ describe("User Endpoint", () => { const data = await res.json(); expect(data.id).toBe(temporaryUser.userId); expect(data.email).toBe(temporaryUser.email); - expect(data.streamToken).toBe("mock-stream-token-123"); - - // Verify Stream Chat createToken was called - expect(mockCreateToken).toHaveBeenCalledTimes(1); - expect(mockCreateToken).toHaveBeenCalledWith(temporaryUser.userId); }); it("should deny unauthenticated access", async () => { @@ -114,63 +81,6 @@ describe("User Endpoint", () => { }); }); - describe("POST /sign-up-to-stream - Sign Up User to Stream Chat", () => { - it("should sign up owner user to stream chat", async () => { - const res = await client.users["sign-up-to-stream"].$post( - {}, - { - headers: { - Authorization: `Bearer ${ownerUser.accessToken}`, - "Content-Type": "application/json", - }, - } - ); - - expect(res.status).toBe(200); - const data = await res.json(); - expect(data.message).toBe("User signed up to stream"); - - // Verify Stream Chat upsertUser was called - expect(mockUpsertUser).toHaveBeenCalledTimes(1); - expect(mockUpsertUser).toHaveBeenCalledWith({ - id: ownerUser.userId, - name: expect.any(String), - language: "fr", - }); - }); - - it("should sign up temp user to stream chat", async () => { - const res = await client.users["sign-up-to-stream"].$post( - {}, - { - headers: { - Authorization: `Bearer ${temporaryUser.accessToken}`, - "Content-Type": "application/json", - }, - } - ); - - expect(res.status).toBe(200); - const data = await res.json(); - expect(data.message).toBe("User signed up to stream"); - - // Verify Stream Chat upsertUser was called - expect(mockUpsertUser).toHaveBeenCalledTimes(1); - expect(mockUpsertUser).toHaveBeenCalledWith({ - id: temporaryUser.userId, - name: expect.any(String), - language: "fr", - }); - }); - - it("should deny unauthenticated stream signup", async () => { - const res = await client.users["sign-up-to-stream"].$post({}); - - expect(res.status).toBe(401); - expect(mockUpsertUser).not.toHaveBeenCalled(); - }); - }); - describe("POST /profile/avatar - Upload Avatar", () => { it("should upload avatar for owner user", async () => { const res = await client.users.profile.avatar.$post( diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts index d7c5b14..38f74f0 100644 --- a/apps/api/src/config.ts +++ b/apps/api/src/config.ts @@ -8,8 +8,6 @@ export interface AppConfig { SUPABASE_SERVICE_ROLE_KEY: string; SUPABASE_CONNECTION_STRING: string; SUPABASE_CA_CERT: string; - STREAM_CHAT_API_KEY: string; - STREAM_CHAT_API_SECRET: string; STRIPE_SECRET_KEY: string; STRIPE_WEBHOOK_SECRET: string; STRIPE_SOLO_PRICE_ID: string; @@ -59,8 +57,6 @@ export function createConfig(secrets?: Secrets): AppConfig { const isTestMode = NODE_ENV === "test"; const isStagingMode = NODE_ENV === "staging"; - const getStreamChatApiSecret = (isStagingMode: boolean) => - isStagingMode ? secrets!.streamChatApiSecretStaging : secrets!.streamChatApiSecret; const getStripeSecretKey = (isStagingMode: boolean) => isStagingMode ? secrets!.stripeSecretKeyStaging : secrets!.stripeSecretKey; const getStripeWebhookSecret = (isStagingMode: boolean) => @@ -82,11 +78,6 @@ export function createConfig(secrets?: Secrets): AppConfig { SUPABASE_CA_CERT: isTestMode ? validateEnvVar("SUPABASE_CA_CERT", process.env.SUPABASE_CA_CERT) : secrets!.supabaseCaCert, - STREAM_CHAT_API_KEY: validateEnvVar("STREAM_CHAT_API_KEY", process.env.STREAM_CHAT_API_KEY), - // Env dependent - STREAM_CHAT_API_SECRET: isTestMode - ? validateEnvVar("STREAM_CHAT_API_SECRET", process.env.STREAM_CHAT_API_SECRET) - : getStreamChatApiSecret(isStagingMode), STRIPE_SECRET_KEY: isTestMode ? validateEnvVar("STRIPE_SECRET_KEY", process.env.STRIPE_SECRET_KEY) : getStripeSecretKeyFromEnv() || getStripeSecretKey(isStagingMode), diff --git a/apps/api/src/helpers/helpers.ts b/apps/api/src/helpers/helpers.ts index eba5984..25ec729 100644 --- a/apps/api/src/helpers/helpers.ts +++ b/apps/api/src/helpers/helpers.ts @@ -3,7 +3,6 @@ import type { SupabaseClient } from "@supabase/supabase-js"; import type { EventAndTablo } from "@xtablo/shared-types"; import type { Context, Next } from "hono"; import type { Transporter } from "nodemailer"; -import type { StreamChat } from "stream-chat"; import { generatePassword } from "./token.js"; export const MAX_TABLO_LIMIT = 10; @@ -290,7 +289,6 @@ export const verifyTabloLimitForUser = async (c: Context, next: Next) => { */ export const createInvitedUser = async ( supabase: SupabaseClient, - streamServerClient: StreamChat, transporter: Transporter, recipientEmail: string, senderEmail: string, @@ -334,12 +332,6 @@ export const createInvitedUser = async ( return { success: false, error: updateProfileError.message }; } - await streamServerClient.upsertUser({ - id: newUser.user.id, - name: recipientEmail.split("@")[0], - language: "fr", - }); - // Send welcome email to the new user await transporter.sendMail({ from: `${senderEmail} via XTablo `, diff --git a/apps/api/src/middlewares/middleware.ts b/apps/api/src/middlewares/middleware.ts index 989e670..773a20b 100644 --- a/apps/api/src/middlewares/middleware.ts +++ b/apps/api/src/middlewares/middleware.ts @@ -4,7 +4,6 @@ import { createClient, type SupabaseClient, type User } from "@supabase/supabase import type { Context, MiddlewareHandler, Next } from "hono"; import { createMiddleware } from "hono/factory"; import type { Transporter } from "nodemailer"; -import { StreamChat } from "stream-chat"; import { Stripe } from "stripe"; import { type AppConfig } from "../config.js"; import { authenticateFromHeader } from "../helpers/auth.js"; @@ -25,9 +24,6 @@ export type Middlewares = { Variables: { supabase: SupabaseClient; user: User }; Bindings: { user: User }; }>; - streamChatMiddleware: MiddlewareHandler<{ - Variables: { streamServerClient: StreamChat }; - }>; r2Middleware: MiddlewareHandler<{ Variables: { s3_client: S3Client }; }>; @@ -168,15 +164,6 @@ export class MiddlewareManager { await next(); }); - const streamChatMiddleware = createMiddleware(async (c: Context, next: Next) => { - const serverClient = StreamChat.getInstance( - config.STREAM_CHAT_API_KEY, - config.STREAM_CHAT_API_SECRET - ); - c.set("streamServerClient", serverClient); - await next(); - }); - const r2Middleware = createMiddleware(async (c: Context, next: Next) => { const s3 = new S3Client({ region: "auto", @@ -255,7 +242,6 @@ export class MiddlewareManager { basicAuthMiddleware, authMiddleware, maybeAuthenticatedMiddleware, - streamChatMiddleware, r2Middleware, regularUserCheckMiddleware, billingCheckoutAccessMiddleware, @@ -282,10 +268,6 @@ export class MiddlewareManager { return this.middlewares.maybeAuthenticatedMiddleware; } - get streamChat() { - return this.middlewares.streamChatMiddleware; - } - get r2() { return this.middlewares.r2Middleware; } diff --git a/apps/api/src/routers/index.ts b/apps/api/src/routers/index.ts index ab4d2a8..1ca996e 100644 --- a/apps/api/src/routers/index.ts +++ b/apps/api/src/routers/index.ts @@ -17,7 +17,6 @@ export const getMainRouter = (config: AppConfig) => { mainRouter.use(middlewareManager.supabase); // Apply remaining middlewares after public routes - mainRouter.use(middlewareManager.streamChat); mainRouter.use(middlewareManager.r2); mainRouter.use(middlewareManager.transporter); mainRouter.use(middlewareManager.stripe); diff --git a/apps/api/src/routers/invite.ts b/apps/api/src/routers/invite.ts index b48c440..b2b47b2 100644 --- a/apps/api/src/routers/invite.ts +++ b/apps/api/src/routers/invite.ts @@ -9,7 +9,6 @@ const factory = createFactory(); const bookSlot = factory.createHandlers(async (c) => { const supabase = c.get("supabase"); - const streamServerClient = c.get("streamServerClient"); const transporter = c.get("transporter"); const maybeUser = c.get("user"); @@ -55,7 +54,6 @@ const bookSlot = factory.createHandlers(async (c) => { // Create a temporary user for the booking const result = await createInvitedUser( supabase, - streamServerClient, transporter, data.user_details.email, ownerData.email, @@ -220,28 +218,6 @@ const bookSlot = factory.createHandlers(async (c) => { return c.json({ error: tabloAccessError.message }, 500); } - // Create Stream chat channel with the owner as creator - const { data: organizationMembers, error: organizationMembersError } = await supabase - .from("profiles") - .select("id") - .eq("organization_id", ownerOrganizationId); - - if (organizationMembersError) { - return c.json({ error: "Failed to load organization members" }, 500); - } - - const channelMembers = Array.from( - new Set((organizationMembers || []).map((member) => member.id).concat(bookerUserDataTyped.id)) - ); - - const channel = streamServerClient.channel("messaging", tabloData.id, { - // @ts-ignore - name: tabloData.name, - created_by_id: ownerId, - members: channelMembers, - }); - await channel.create(); - const newEvent: TablesInsert<"events"> = { description: eventTypeConfig.description || "", end_time: data.event_details.end_time || "", @@ -258,12 +234,6 @@ const bookSlot = factory.createHandlers(async (c) => { return c.json({ error: "Failed to create event" }, 500); } - // Send a welcome message to the channel - await channel.sendMessage({ - text: `🎉 Bienvenue dans votre nouveau tablo "${tabloData.name}" ! Votre rendez-vous "${newEvent.title}" est confirmé pour le ${newEvent.start_date} de ${newEvent.start_time} à ${newEvent.end_time}.`, - user_id: ownerId, - }); - // Send email notifications to both owner and invited user // Send email to the owner await transporter.sendMail({ diff --git a/apps/api/src/routers/tablo.ts b/apps/api/src/routers/tablo.ts index 43f6ace..5ce02b7 100644 --- a/apps/api/src/routers/tablo.ts +++ b/apps/api/src/routers/tablo.ts @@ -18,83 +18,6 @@ type PostTablo = Omit & { const factory = createFactory(); -const isAlreadyMemberError = (error: unknown): boolean => { - if (!error) return false; - const message = (error instanceof Error ? error.message : String(error)).toLowerCase(); - return ( - message.includes("already a member") || - message.includes("already member") || - message.includes("member already exists") - ); -}; - -const upsertStreamUserFromProfile = async ( - supabase: AuthEnv["Variables"]["supabase"], - streamServerClient: AuthEnv["Variables"]["streamServerClient"], - userId: string -) => { - const { data: profile } = await supabase - .from("profiles") - .select("name") - .eq("id", userId) - .maybeSingle(); - - await streamServerClient.upsertUser({ - id: userId, - name: profile?.name ?? "", - language: "fr", - }); -}; - -const ensureTabloChannelMember = async ( - supabase: AuthEnv["Variables"]["supabase"], - streamServerClient: AuthEnv["Variables"]["streamServerClient"], - tabloId: string, - userId: string -) => { - const channel = streamServerClient.channel("messaging", tabloId); - - try { - await channel.addMembers([userId]); - return; - } catch (error) { - if (isAlreadyMemberError(error)) { - return; - } - } - - const { data: tablo } = await supabase - .from("tablos") - .select("name, owner_id") - .eq("id", tabloId) - .maybeSingle(); - - const { data: accessRows } = await supabase - .from("tablo_access") - .select("user_id") - .eq("tablo_id", tabloId) - .eq("is_active", true); - - const members = Array.from(new Set((accessRows || []).map((row) => row.user_id).concat(userId))); - - const channelToCreate = streamServerClient.channel("messaging", tabloId, { - // @ts-ignore - name: tablo?.name ?? "Tablo", - created_by_id: tablo?.owner_id ?? userId, - members, - }); - - try { - await channelToCreate.create(); - } catch (error) { - if (isAlreadyMemberError(error)) { - return; - } - - await channel.addMembers([userId]); - } -}; - const createTablo = (middlewareManager: ReturnType) => factory.createHandlers( middlewareManager.regularUserCheck, @@ -134,28 +57,6 @@ const createTablo = (middlewareManager: ReturnType; - const { data: organizationMembers, error: membersError } = await supabase - .from("profiles") - .select("id") - .eq("organization_id", profile.organization_id); - - if (membersError) { - return c.json({ error: "Failed to load organization members" }, 500); - } - - const channelMembers = Array.from( - new Set((organizationMembers || []).map((member) => member.id).concat(user.id)) - ); - - const streamServerClient = c.get("streamServerClient"); - const channel = streamServerClient.channel("messaging", tabloData.id, { - // @ts-ignore - name: tabloData.name, - created_by_id: user.id, - members: channelMembers, - }); - await channel.create(); - if (typedPayload.events) { const eventsToInsert = typedPayload.events.map((event) => ({ ...event, @@ -173,7 +74,6 @@ const updateTablo = (middlewareManager: ReturnType { const user = c.get("user"); const supabase = c.get("supabase"); - const streamServerClient = c.get("streamServerClient"); const data = await c.req.json(); const { id, ...tablo } = data; @@ -190,7 +90,7 @@ const updateTablo = (middlewareManager: ReturnType; - const isUpdatingName = tablo.name !== undefined; - - if (isUpdatingName) { - const channel = streamServerClient.channel("messaging", updatedTablo.id); - try { - await channel.update({ - // @ts-ignore - name: updatedTablo.name, - }); - } catch (error) { - console.error("error updating channel", error); - } - } - return c.json({ message: "Tablo updated successfully" }); }); const deleteTablo = factory.createHandlers(async (c) => { const user = c.get("user"); const supabase = c.get("supabase"); - const streamServerClient = c.get("streamServerClient"); const data = await c.req.json(); const { id } = data; @@ -270,13 +154,6 @@ const deleteTablo = factory.createHandlers(async (c) => { return c.json({ error: error.message }, 500); } - const channel = streamServerClient.channel("messaging", id); - try { - await channel.delete(); - } catch (error) { - console.error("error deleting channel", error); - } - return c.json({ message: "Tablo deleted successfully" }); }); @@ -288,7 +165,6 @@ const inviteToTablo = ( const transporter = c.get("transporter"); const sender = c.get("user"); const supabase = c.get("supabase"); - const streamServerClient = c.get("streamServerClient"); const tabloId = c.req.param("tabloId"); const { email: recipientmail } = await c.req.json(); @@ -355,7 +231,6 @@ const inviteToTablo = ( // Create a new invited user and add them to the tablo const result = await createInvitedUser( supabase, - streamServerClient, transporter, recipientEmail, sender.email, @@ -381,13 +256,6 @@ const inviteToTablo = ( return c.json({ error: tabloAccessError.message }, 500); } - try { - await ensureTabloChannelMember(supabase, streamServerClient, tabloId, result.userId); - } catch (streamError) { - console.error("error adding temporary invited user to channel", streamError); - return c.json({ error: "Failed to sync chat access for invited user" }, 500); - } - return c.json({ message: "User created and invite sent successfully", }); @@ -438,7 +306,6 @@ const cancelPendingInvite = (middlewareManager: ReturnType { const user = c.get("user"); const supabase = c.get("supabase"); - const streamServerClient = c.get("streamServerClient"); const tabloId = c.req.param("tabloId"); const inviteId = Number(c.req.param("inviteId")); @@ -513,13 +380,6 @@ const cancelPendingInvite = (middlewareManager: ReturnType { const user = c.get("user"); const supabase = c.get("supabase"); - const streamServerClient = c.get("streamServerClient"); const inviteId = Number(c.req.param("inviteId")); if (!Number.isInteger(inviteId) || inviteId <= 0) { @@ -598,13 +457,6 @@ const acceptInviteById = (middlewareManager: ReturnType { const joiner = c.get("user"); const supabase = c.get("supabase"); - const streamServerClient = c.get("streamServerClient"); const { data: inviteData, error } = await supabase .from("tablo_invites") @@ -657,13 +501,6 @@ const joinTablo = factory.createHandlers(async (c) => { const { id: invite_id, tablo_id, invited_by } = inviteData; - try { - await upsertStreamUserFromProfile(supabase, streamServerClient, joiner.id); - } catch (error) { - console.error("error upserting joining user to stream", error); - return c.json({ error: "Failed to provision chat user" }, 500); - } - const { error: tabloAccessError } = await supabase.from("tablo_access").insert({ tablo_id, user_id: joiner.id, @@ -686,13 +523,6 @@ const joinTablo = factory.createHandlers(async (c) => { // Mark invite as accepted instead of deleting (maintains audit trail) await supabase.from("tablo_invites").update({ is_pending: false }).eq("id", invite_id); - try { - await ensureTabloChannelMember(supabase, streamServerClient, tablo_id, joiner.id); - } catch (error) { - console.error("error adding member to channel", error); - return c.json({ error: "Failed to sync chat access for this tablo" }, 500); - } - return c.json({ tablo_id }); }); @@ -748,12 +578,8 @@ const getTabloMembers = factory.createHandlers(async (c) => { const leaveTablo = factory.createHandlers(async (c) => { const user = c.get("user"); const supabase = c.get("supabase"); - const streamServerClient = c.get("streamServerClient"); const { tablo_id } = await c.req.json(); - const channel = streamServerClient.channel("messaging", tablo_id); - await channel.removeMembers([user.id]); - const { error } = await supabase .from("tablo_access") .update({ is_active: false }) @@ -872,7 +698,6 @@ export const getTabloRouter = (config: AppConfig) => { tabloRouter.use(middlewareManager.supabase); tabloRouter.use(middlewareManager.auth); - tabloRouter.use(middlewareManager.streamChat); tabloRouter.post("/create", ...createTablo(middlewareManager)); tabloRouter.patch("/update", ...updateTablo(middlewareManager)); diff --git a/apps/api/src/routers/tasks.ts b/apps/api/src/routers/tasks.ts index 428f313..fb17794 100644 --- a/apps/api/src/routers/tasks.ts +++ b/apps/api/src/routers/tasks.ts @@ -39,7 +39,6 @@ const syncCalendars = factory.createHandlers(async (c) => { const syncTabloNames = factory.createHandlers(async (c) => { const supabase = c.get("supabase"); - const streamServerClient = c.get("streamServerClient"); const fifteenMinutesInMilliseconds = 1000 * 60 * 15; @@ -54,18 +53,6 @@ const syncTabloNames = factory.createHandlers(async (c) => { const tablosData = data as { id: string; name: string }[]; - tablosData.forEach(async (tablo) => { - const channel = streamServerClient.channel("messaging", tablo.id); - try { - await channel.update({ - // @ts-ignore - name: tablo.name, - }); - } catch (error) { - console.error(`error updating channel, tablo id: ${tablo.id}, error: ${error}`); - } - }); - return c.json({ message: `Synced ${tablosData.length} tablo names` }); }); diff --git a/apps/api/src/routers/user.ts b/apps/api/src/routers/user.ts index 7616c79..f5cdde4 100644 --- a/apps/api/src/routers/user.ts +++ b/apps/api/src/routers/user.ts @@ -11,30 +11,9 @@ const factory = createFactory(); const isMissingRelationError = (code: string | undefined) => code === "42P01" || code === "PGRST205"; -const signUpToStream = factory.createHandlers(async (c) => { - const { id } = c.get("user"); - const supabase = c.get("supabase"); - - const { data } = await supabase.from("profiles").select("*").eq("id", id).single(); - - const user = data as Tables<"profiles">; - - const streamServerClient = c.get("streamServerClient"); - await streamServerClient.upsertUser({ - id, - name: user.name ?? "", - language: "fr", - }); - - return c.json({ - message: "User signed up to stream", - }); -}); - const getMe = factory.createHandlers(async (c) => { const user = c.get("user"); const supabase = c.get("supabase"); - const streamServerClient = c.get("streamServerClient"); const { data, error } = await supabase.from("profiles").select("*").eq("id", user.id).single(); @@ -60,14 +39,7 @@ const getMe = factory.createHandlers(async (c) => { effectivePlan = organizationPlan; } - const user_id = data.id; - const token = streamServerClient.createToken(user_id); - - return c.json({ - ...userData, - plan: effectivePlan, - streamToken: token, - }); + return c.json({ ...userData, plan: effectivePlan }); }); const markTemporary = factory.createHandlers(async (c) => { @@ -515,7 +487,6 @@ const inviteToOrganization = factory.createHandlers(async (c) => { const user = c.get("user"); const supabase = c.get("supabase"); const transporter = c.get("transporter"); - const streamServerClient = c.get("streamServerClient"); const body = await c.req.json(); const rawEmail = typeof body?.email === "string" ? body.email : ""; const recipientEmail = rawEmail.trim().toLowerCase(); @@ -613,7 +584,6 @@ const inviteToOrganization = factory.createHandlers(async (c) => { const invitedUser = await createInvitedUser( supabase, - streamServerClient, transporter, recipientEmail, senderProfile.email, @@ -673,15 +643,6 @@ const inviteToOrganization = factory.createHandlers(async (c) => { } } - for (const tablo of organizationTablos || []) { - const channel = streamServerClient.channel("messaging", tablo.id); - try { - await channel.addMembers([invitedUser.userId]); - } catch (error) { - console.error("Failed to add invited user to Stream channel:", error); - } - } - if (oldOrganizationId && oldOrganizationId !== organizationId) { const { count: oldOrgMembersCount } = await supabase .from("profiles") @@ -717,7 +678,6 @@ const inviteToOrganization = factory.createHandlers(async (c) => { const removeOrganizationMember = factory.createHandlers(async (c) => { const user = c.get("user"); const supabase = c.get("supabase"); - const streamServerClient = c.get("streamServerClient"); const memberId = c.req.param("memberId"); if (!memberId) { @@ -826,14 +786,6 @@ const removeOrganizationMember = factory.createHandlers(async (c) => { return c.json({ error: "Failed to revoke member tablo permissions" }, 500); } - for (const tabloId of tabloIds) { - try { - const channel = streamServerClient.channel("messaging", tabloId); - await channel.removeMembers([memberId]); - } catch (error) { - console.error("Failed to remove organization member from Stream channel:", error); - } - } } const { error: inviteCleanupError } = await supabase @@ -852,7 +804,6 @@ const removeOrganizationMember = factory.createHandlers(async (c) => { export const getUserRouter = () => { const userRouter = new Hono(); - userRouter.post("/sign-up-to-stream", ...signUpToStream); userRouter.get("/me", ...getMe); userRouter.post("/mark-temporary", ...markTemporary); userRouter.post("/profile/avatar", ...uploadAvatar); diff --git a/apps/api/src/secrets.ts b/apps/api/src/secrets.ts index 4a69b8e..2126133 100644 --- a/apps/api/src/secrets.ts +++ b/apps/api/src/secrets.ts @@ -26,11 +26,9 @@ export type Secrets = { r2AccessKeyId: string; r2SecretAccessKey: string; // Env dependent - streamChatApiSecret: string; stripeSecretKey: string; stripeWebhookSecret: string; // Staging - streamChatApiSecretStaging: string; stripeSecretKeyStaging: string; stripeWebhookSecretStaging: string; }; @@ -50,11 +48,9 @@ export async function loadSecrets(): Promise { r2SecretAccessKey: await fetchSecret("r2-secret-access-key"), // Env dependent // Staging - streamChatApiSecretStaging: await fetchSecret("stream-chat-api-secret-staging"), stripeSecretKeyStaging: await fetchSecret("stripe-secret-key-staging"), stripeWebhookSecretStaging: await fetchSecret("stripe-webhook-secret-staging"), // Production - streamChatApiSecret: await fetchSecret("stream-chat-api-secret"), stripeSecretKey: await fetchSecret("stripe-secret-key"), stripeWebhookSecret: await fetchSecret("stripe-webhook-secret"), }; diff --git a/apps/api/src/types/app.types.ts b/apps/api/src/types/app.types.ts index f895966..1f14da7 100644 --- a/apps/api/src/types/app.types.ts +++ b/apps/api/src/types/app.types.ts @@ -3,7 +3,6 @@ import type { StripeSync } from "@supabase/stripe-sync-engine"; import type { SupabaseClient, User } from "@supabase/supabase-js"; import type { Hono } from "hono"; import type { Transporter } from "nodemailer"; -import type { StreamChat } from "stream-chat"; import type Stripe from "stripe"; /** @@ -12,7 +11,6 @@ import type Stripe from "stripe"; export type BaseEnv = { Variables: { supabase: SupabaseClient; - streamServerClient: StreamChat; s3_client: S3Client; transporter: Transporter; stripe: Stripe;