From afb91ae84aec663d1b20c1e476f75c5a67f06e1c Mon Sep 17 00:00:00 2001 From: Makisuo Date: Sat, 31 Oct 2026 18:12:11 +0100 Subject: [PATCH] fix: authorize org routes by membership; drop CurrentUser.organizationId CurrentUser.organizationId was never set (sessions, OAuth bearers and bot tokens all leave it empty), so every check against it failed: the T3 Code link, integration OAuth start, connection status and disconnect answered 401 for every member, and web pages that read it never loaded their org's connections, mentionable bots or commands. The field is gone from CurrentUser (backend and web), so no code can rely on it again. The integration and T3 Code routes check the directory's membership index (requireMember); the web reads the route's organization. Also bundle T3 Code's own logo instead of Brandfetch's t3.chat icon. Co-Authored-By: Claude Opus 5.5 --- apps/backend/src/policies/bot-policy.test.ts | 3 +- .../src/policies/policy-test-helpers.ts | 1 - apps/backend/src/routes/api-v1/bot-auth.ts | 1 - .../src/routes/http-success-encoding.test.ts | 1 - apps/backend/src/routes/integrations.http.ts | 28 +++---------------- apps/backend/src/routes/object-routes.test.ts | 1 - apps/backend/src/routes/t3code.http.ts | 14 ++-------- apps/backend/src/rpc/handlers/bots.ts | 12 +++----- apps/backend/src/rpc/middleware/auth.test.ts | 1 - apps/backend/src/rpc/middleware/auth.ts | 1 - apps/backend/src/rpc/per-request.test.ts | 1 - apps/backend/src/rpc/routing.ts | 10 +++---- .../chat-sync/chat-sync.objects.test.ts | 1 - apps/backend/src/services/hazel-session.ts | 1 - apps/backend/src/services/invitations.test.ts | 1 - .../backend/src/services/oauth-bearer-auth.ts | 1 - apps/backend/src/worker/live/live.test.ts | 1 - .../worker/objects/channel-messages.test.ts | 1 - .../worker/objects/connect-objects.test.ts | 1 - .../src/worker/objects/cross-org.test.ts | 1 - .../src/worker/objects/directory-path.test.ts | 1 - .../src/worker/objects/html-renders.test.ts | 1 - .../src/worker/objects/message-search.test.ts | 1 - .../src/worker/objects/objects.test.ts | 1 - .../src/worker/objects/org-objects.test.ts | 2 -- .../src/worker/objects/org/organization.ts | 11 +++----- .../public/images/integrations/t3code.svg | 1 + apps/web-foldkit/src/app/command.ts | 1 - .../src/core/lib/integrations/__data.ts | 4 +-- .../chat/channel/fixtures.test-support.ts | 1 - .../src/page/chat/channel/subscription.ts | 4 +-- .../src/page/chat/channel/write.ts | 2 +- apps/web-foldkit/src/page/home/story.test.ts | 1 - .../my-settings/linked-accounts/story.test.ts | 4 +-- .../linked-accounts/subscription.ts | 2 +- .../my-settings/linked-accounts/update.ts | 2 +- .../my-settings/notifications/story.test.ts | 1 - .../my-settings/profile/profile.scene.test.ts | 1 - .../my-settings/profile/profile.story.test.ts | 1 - .../src/page/onboarding/scene.test.ts | 1 - apps/web-foldkit/src/page/root/index.ts | 21 ++++---------- .../integrations/index/subscription.ts | 5 ++-- .../integrations/installed/subscription.ts | 2 +- .../integrations/marketplace/subscription.ts | 2 +- apps/web-foldkit/src/route.test.ts | 1 - apps/web-foldkit/src/session.ts | 2 -- .../src/test/pages-entry-fixtures.ts | 2 +- apps/web-foldkit/src/test/pages-fixtures.ts | 1 - apps/web-foldkit/src/test/root-fixtures.ts | 1 - packages/domain/src/current-user.ts | 3 +- 50 files changed, 41 insertions(+), 124 deletions(-) create mode 100644 apps/web-foldkit/public/images/integrations/t3code.svg diff --git a/apps/backend/src/policies/bot-policy.test.ts b/apps/backend/src/policies/bot-policy.test.ts index b539ffe89..1dc16b101 100644 --- a/apps/backend/src/policies/bot-policy.test.ts +++ b/apps/backend/src/policies/bot-policy.test.ts @@ -67,7 +67,6 @@ describe("BotPolicy", () => { }) const outsider = makeActor({ id: "00000000-0000-4000-8000-000000000403" as UserId, - organizationId: TEST_ORG_ID, }) const layer = makePolicyLayer( @@ -88,7 +87,7 @@ describe("BotPolicy", () => { const adminDenied = await runWithActorEither( BotPolicy.use((policy) => policy.canRead(BOT_ID)), layer, - makeActor({ ...admin, organizationId: TEST_ORG_ID }), + admin, ) const outsiderDenied = await runWithActorEither( BotPolicy.use((policy) => policy.canRead(BOT_ID)), diff --git a/apps/backend/src/policies/policy-test-helpers.ts b/apps/backend/src/policies/policy-test-helpers.ts index 73afa88be..0a0c517f5 100644 --- a/apps/backend/src/policies/policy-test-helpers.ts +++ b/apps/backend/src/policies/policy-test-helpers.ts @@ -20,7 +20,6 @@ export const makeActor = (overrides?: Partial): CurrentUser. role: "member", isOnboarded: true, timezone: "UTC", - organizationId: TEST_ORG_ID, settings: null, ...overrides, }) diff --git a/apps/backend/src/routes/api-v1/bot-auth.ts b/apps/backend/src/routes/api-v1/bot-auth.ts index d421f79ae..74817559f 100644 --- a/apps/backend/src/routes/api-v1/bot-auth.ts +++ b/apps/backend/src/routes/api-v1/bot-auth.ts @@ -57,7 +57,6 @@ export const botPrincipal = (bot: { email: `bot-${bot.name}@hazel.bot`, isOnboarded: true, timezone: null, - organizationId: null, settings: null, }), botScopes: bot.scopes === null ? null : bot.scopes.filter(isApiScope), diff --git a/apps/backend/src/routes/http-success-encoding.test.ts b/apps/backend/src/routes/http-success-encoding.test.ts index f332d1de1..d99463ce6 100644 --- a/apps/backend/src/routes/http-success-encoding.test.ts +++ b/apps/backend/src/routes/http-success-encoding.test.ts @@ -15,7 +15,6 @@ import { CurrentUser } from "@hazel/domain" const sessionUser = (id: UserId) => new CurrentUser.Schema({ id, - organizationId: null, role: "member", email: "caller@example.com", isOnboarded: true, diff --git a/apps/backend/src/routes/integrations.http.ts b/apps/backend/src/routes/integrations.http.ts index 4888f1245..f91e6810f 100644 --- a/apps/backend/src/routes/integrations.http.ts +++ b/apps/backend/src/routes/integrations.http.ts @@ -21,6 +21,7 @@ import { Cause, Config, DateTime, Effect, Layer, Option, Schedule, Schema, Schem import * as Duration from "effect/Duration" import { HazelApi } from "../api" import { Directory } from "../services/directory" +import { requireMember } from "./integration-resources.http" import { OAuthProviderRegistry } from "../services/oauth" import { callObject } from "../worker/objects/calls" import { orgTarget } from "../worker/objects/kinds" @@ -270,14 +271,7 @@ const handleGetOAuthUrl = Effect.fn("integrations.getOAuthUrl")(function* ( const { orgId, provider } = path const level = query.level ?? "organization" - if (!currentUser.organizationId || currentUser.organizationId !== orgId) { - return yield* Effect.fail( - new UnauthorizedError({ - message: "You are not authorized to access this organization", - detail: `organizationId=${orgId}`, - }), - ) - } + yield* requireMember(orgId) // Get the OAuth provider from registry const registry = yield* OAuthProviderRegistry @@ -913,14 +907,7 @@ const handleGetConnectionStatus = Effect.fn("integrations.getConnectionStatus")( // A T3 Code link is a member's own, with its own routes (`./t3code.http.ts`). if (provider === "t3code") return yield* Effect.fail(new UnsupportedProviderError({ provider })) - if (!currentUser.organizationId || currentUser.organizationId !== orgId) { - return yield* Effect.fail( - new UnauthorizedError({ - message: "You are not authorized to access this organization", - detail: `organizationId=${orgId}`, - }), - ) - } + yield* requireMember(orgId) const connectionOption = Option.fromNullishOr( yield* callObject(IntegrationConnectionCall, orgTarget(orgId), { @@ -967,14 +954,7 @@ const handleDisconnect = Effect.fn("integrations.disconnect")(function* ( // A T3 Code link is a member's own, with its own routes (`./t3code.http.ts`). if (provider === "t3code") return yield* Effect.fail(new UnsupportedProviderError({ provider })) - if (!currentUser.organizationId || currentUser.organizationId !== orgId) { - return yield* Effect.fail( - new UnauthorizedError({ - message: "You are not authorized to access this organization", - detail: `organizationId=${orgId}`, - }), - ) - } + yield* requireMember(orgId) // Tokens, the connection and (for a user link) re-attribution, in the org's object. const connection = yield* callObject(DisconnectIntegrationCall, orgTarget(orgId), { diff --git a/apps/backend/src/routes/object-routes.test.ts b/apps/backend/src/routes/object-routes.test.ts index 97a3d8cee..794ab6a2a 100644 --- a/apps/backend/src/routes/object-routes.test.ts +++ b/apps/backend/src/routes/object-routes.test.ts @@ -190,7 +190,6 @@ const invokeAs = (object: TestObject, objectId: string, tag: HazelRpcTag, payloa principal: yield* encodeInvokePrincipal({ user: new CurrentUser.Schema({ id: userId, - organizationId: null, role: "member", email: `${userId}@example.com`, isOnboarded: true, diff --git a/apps/backend/src/routes/t3code.http.ts b/apps/backend/src/routes/t3code.http.ts index fee5b6109..7488aca10 100644 --- a/apps/backend/src/routes/t3code.http.ts +++ b/apps/backend/src/routes/t3code.http.ts @@ -42,6 +42,7 @@ import { UpdateT3CodeSharingCall, } from "../worker/objects/org/t3code" import { validateBotToken } from "./bot-commands.http" +import { requireMember } from "./integration-resources.http" /** The seeded T3 Code bot's directory user (`IntegrationBotService`); the only caller of `getT3CodeAccess`. */ export const T3CODE_BOT_EXTERNAL_ID = "internal-bot-t3code" @@ -110,17 +111,8 @@ const environmentConfig = Effect.gen(function* () { const redirectUriOf = (apiBaseUrl: string) => `${new URL(apiBaseUrl).origin}/integrations/t3code/callback` -const requireOrg = (orgId: OrganizationId) => - Effect.gen(function* () { - const currentUser = yield* CurrentUser.Context - if (currentUser.organizationId !== orgId) { - return yield* new UnauthorizedError({ - message: "You are not authorized to access this organization", - detail: `organizationId=${orgId}`, - }) - } - return currentUser - }) +/** The caller, once they are an active member of `orgId` (sessions carry no organization). */ +const requireOrg = (orgId: OrganizationId) => requireMember(orgId).pipe(Effect.andThen(CurrentUser.Context)) const toDateTime = (value: Date | DateTime.Utc) => DateTime.isDateTime(value) ? value : DateTime.fromDateUnsafe(value) diff --git a/apps/backend/src/rpc/handlers/bots.ts b/apps/backend/src/rpc/handlers/bots.ts index 59791f32d..042ffd0b8 100644 --- a/apps/backend/src/rpc/handlers/bots.ts +++ b/apps/backend/src/rpc/handlers/bots.ts @@ -58,11 +58,9 @@ const generateBotToken = Effect.promise(async () => { return { token, tokenHash: Hex.encode(new Uint8Array(digest)) } }) -/** The org a bot call acts in: the routing hint, else the caller's active org. */ -const activeOrganization = (hint: OrganizationId | undefined) => +/** The org a bot call acts in: the routing hint. */ +const activeOrganization = (organizationId: OrganizationId | undefined) => Effect.gen(function* () { - const user = yield* CurrentUser.Context - const organizationId = hint ?? user.organizationId ?? undefined // The router sends calls with an org to its OrgObject; anywhere else there is none. const object = yield* currentObject if (organizationId === undefined || Option.isNone(object) || object.value.kind !== "org") { @@ -317,21 +315,19 @@ export const BotRpcLive = BotRpcs.toLayer( "bot.listPublic": ({ search, organizationId }) => Effect.gen(function* () { - const user = yield* CurrentUser.Context const botRepo = yield* BotRepo const userRepo = yield* UserRepo const directory = yield* Directory const publicBots = yield* botRepo.findPublic(search) // Installed here = the bot user is an active member (`membership_index`). - const orgId = organizationId ?? user.organizationId ?? undefined const installed = new Set() - if (orgId !== undefined && publicBots.length > 0) { + if (organizationId !== undefined && publicBots.length > 0) { const rows = yield* directory.db.run( SQLite.from(schema.membershipIndexTable) .select("userId") .where(($) => [ - $.organizationId.eq(orgId), + $.organizationId.eq(organizationId), $.deletedAt.isNull(), $.userId.in_(...publicBots.map((bot) => bot.userId)), ]), diff --git a/apps/backend/src/rpc/middleware/auth.test.ts b/apps/backend/src/rpc/middleware/auth.test.ts index 067b253e1..904faaf43 100644 --- a/apps/backend/src/rpc/middleware/auth.test.ts +++ b/apps/backend/src/rpc/middleware/auth.test.ts @@ -32,7 +32,6 @@ const makeCurrentUser = ( role: "member", isOnboarded: true, timezone: "UTC", - organizationId: null, settings: null, ...overrides, }) diff --git a/apps/backend/src/rpc/middleware/auth.ts b/apps/backend/src/rpc/middleware/auth.ts index ce9769bd6..221fb1d98 100644 --- a/apps/backend/src/rpc/middleware/auth.ts +++ b/apps/backend/src/rpc/middleware/auth.ts @@ -133,7 +133,6 @@ export const AuthMiddlewareLive = Layer.effect( role: "member", isOnboarded: true, timezone: user.timezone, - organizationId: null, settings: user.settings, } diff --git a/apps/backend/src/rpc/per-request.test.ts b/apps/backend/src/rpc/per-request.test.ts index 26ea158a7..4ccd7d442 100644 --- a/apps/backend/src/rpc/per-request.test.ts +++ b/apps/backend/src/rpc/per-request.test.ts @@ -46,7 +46,6 @@ const requestScheduler = (label: string): Scheduler.Scheduler => ({ const user = new CurrentUser.Schema({ id: "00000000-0000-4000-8000-000000000001" as UserId, - organizationId: null, role: "member", email: "me@example.com", isOnboarded: true, diff --git a/apps/backend/src/rpc/routing.ts b/apps/backend/src/rpc/routing.ts index f6e31ea3d..3fb7a531a 100644 --- a/apps/backend/src/rpc/routing.ts +++ b/apps/backend/src/rpc/routing.ts @@ -91,13 +91,11 @@ const orgHint = (method: string) => (payload: { readonly organizationId?: Organi Effect.flatMap(hint(method, "organizationId", payload.organizationId), org) /** - * The org a bot call acts in: the payload's `organizationId`, else the caller's active org; with - * neither, the directory path (which answers `ActiveOrganizationRequiredError`, or `[]`). + * The org a bot call acts in: the payload's `organizationId`; without one, the directory path + * (which answers `ActiveOrganizationRequiredError`, or `[]`). */ -const activeOrg = (payload: { readonly organizationId?: OrganizationId | undefined }, current: User) => { - const organizationId = payload.organizationId ?? current.organizationId ?? undefined - return organizationId === undefined ? directory() : org(organizationId) -} +const activeOrg = (payload: { readonly organizationId?: OrganizationId | undefined }) => + payload.organizationId === undefined ? directory() : org(payload.organizationId) const byOrg = (payload: { readonly organizationId: OrganizationId }) => org(payload.organizationId) const byChannel = (payload: { readonly channelId: ChannelId }) => channel(payload.channelId) diff --git a/apps/backend/src/services/chat-sync/chat-sync.objects.test.ts b/apps/backend/src/services/chat-sync/chat-sync.objects.test.ts index 0b80ac83a..97201633a 100644 --- a/apps/backend/src/services/chat-sync/chat-sync.objects.test.ts +++ b/apps/backend/src/services/chat-sync/chat-sync.objects.test.ts @@ -39,7 +39,6 @@ const DISCORD_CHANNEL = "222222222222222222" as ExternalChannelId const userOf = (id: string) => new CurrentUser.Schema({ id: id as UserId, - organizationId: null, role: "member", email: `${id}@example.com`, isOnboarded: true, diff --git a/apps/backend/src/services/hazel-session.ts b/apps/backend/src/services/hazel-session.ts index 7c5b5d29f..0e68a384f 100644 --- a/apps/backend/src/services/hazel-session.ts +++ b/apps/backend/src/services/hazel-session.ts @@ -59,7 +59,6 @@ export class HazelSession extends Context.Service()("@hazel/backen new CurrentUser.Schema({ id: user.id, role: "member", - organizationId: undefined, avatarUrl: user.avatarUrl ?? undefined, firstName: user.firstName, lastName: user.lastName, diff --git a/apps/backend/src/services/invitations.test.ts b/apps/backend/src/services/invitations.test.ts index 8d1640651..9953ce189 100644 --- a/apps/backend/src/services/invitations.test.ts +++ b/apps/backend/src/services/invitations.test.ts @@ -31,7 +31,6 @@ const actor = (userId: UserId) => new CurrentUser.Schema({ id: userId, role: "member", - organizationId: undefined, avatarUrl: undefined, firstName: "Test", lastName: "User", diff --git a/apps/backend/src/services/oauth-bearer-auth.ts b/apps/backend/src/services/oauth-bearer-auth.ts index 7a4e46772..124202f5e 100644 --- a/apps/backend/src/services/oauth-bearer-auth.ts +++ b/apps/backend/src/services/oauth-bearer-auth.ts @@ -112,7 +112,6 @@ export class OAuthBearerAuth extends Context.Service()("@hazel/ currentUser: new CurrentUser.Schema({ id: user.id, role: "member", - organizationId: undefined, avatarUrl: user.avatarUrl ?? undefined, firstName: user.firstName, lastName: user.lastName, diff --git a/apps/backend/src/worker/live/live.test.ts b/apps/backend/src/worker/live/live.test.ts index 313697526..06c349e01 100644 --- a/apps/backend/src/worker/live/live.test.ts +++ b/apps/backend/src/worker/live/live.test.ts @@ -32,7 +32,6 @@ const run = (effect: Effect.Effect) => const principalOf = (id: UserId): InvokePrincipal => ({ user: new CurrentUser.Schema({ id, - organizationId: null, role: "member", email: `${id}@example.com`, isOnboarded: true, diff --git a/apps/backend/src/worker/objects/channel-messages.test.ts b/apps/backend/src/worker/objects/channel-messages.test.ts index 795575069..ef4557a5f 100644 --- a/apps/backend/src/worker/objects/channel-messages.test.ts +++ b/apps/backend/src/worker/objects/channel-messages.test.ts @@ -18,7 +18,6 @@ const uuid = () => crypto.randomUUID() const userOf = (id: string) => new CurrentUser.Schema({ id: id as UserId, - organizationId: null, role: "member", email: `${id}@example.com`, isOnboarded: true, diff --git a/apps/backend/src/worker/objects/connect-objects.test.ts b/apps/backend/src/worker/objects/connect-objects.test.ts index ecb606e17..20c3c5dfb 100644 --- a/apps/backend/src/worker/objects/connect-objects.test.ts +++ b/apps/backend/src/worker/objects/connect-objects.test.ts @@ -23,7 +23,6 @@ const uuid = () => crypto.randomUUID() const userOf = (id: string) => new CurrentUser.Schema({ id: id as UserId, - organizationId: null, role: "member", email: `${id}@example.com`, isOnboarded: true, diff --git a/apps/backend/src/worker/objects/cross-org.test.ts b/apps/backend/src/worker/objects/cross-org.test.ts index 4e4a0e4d7..2ce295318 100644 --- a/apps/backend/src/worker/objects/cross-org.test.ts +++ b/apps/backend/src/worker/objects/cross-org.test.ts @@ -23,7 +23,6 @@ const uuid = () => crypto.randomUUID() const userOf = (id: string) => new CurrentUser.Schema({ id: id as UserId, - organizationId: null, role: "member", email: `${id}@example.com`, isOnboarded: true, diff --git a/apps/backend/src/worker/objects/directory-path.test.ts b/apps/backend/src/worker/objects/directory-path.test.ts index bdd071401..3d86372e5 100644 --- a/apps/backend/src/worker/objects/directory-path.test.ts +++ b/apps/backend/src/worker/objects/directory-path.test.ts @@ -25,7 +25,6 @@ const uuid = () => crypto.randomUUID() const userOf = (id: string, email = `${id}@example.com`) => new CurrentUser.Schema({ id: id as UserId, - organizationId: null, role: "member", email, isOnboarded: true, diff --git a/apps/backend/src/worker/objects/html-renders.test.ts b/apps/backend/src/worker/objects/html-renders.test.ts index f0eb8d6a9..dd883371c 100644 --- a/apps/backend/src/worker/objects/html-renders.test.ts +++ b/apps/backend/src/worker/objects/html-renders.test.ts @@ -17,7 +17,6 @@ const uuid = () => crypto.randomUUID() const userOf = (id: string) => new CurrentUser.Schema({ id: id as UserId, - organizationId: null, role: "member", email: `${id}@example.com`, isOnboarded: true, diff --git a/apps/backend/src/worker/objects/message-search.test.ts b/apps/backend/src/worker/objects/message-search.test.ts index 7a699c1a2..5abdec8b0 100644 --- a/apps/backend/src/worker/objects/message-search.test.ts +++ b/apps/backend/src/worker/objects/message-search.test.ts @@ -29,7 +29,6 @@ const uuid = () => crypto.randomUUID() const userOf = (id: string) => new CurrentUser.Schema({ id: id as UserId, - organizationId: null, role: "member", email: `${id}@example.com`, isOnboarded: true, diff --git a/apps/backend/src/worker/objects/objects.test.ts b/apps/backend/src/worker/objects/objects.test.ts index 160b5fb64..2983a7650 100644 --- a/apps/backend/src/worker/objects/objects.test.ts +++ b/apps/backend/src/worker/objects/objects.test.ts @@ -22,7 +22,6 @@ const uuid = () => crypto.randomUUID() const userOf = (id: string) => new CurrentUser.Schema({ id: id as UserId, - organizationId: null, role: "member", email: `${id}@example.com`, isOnboarded: true, diff --git a/apps/backend/src/worker/objects/org-objects.test.ts b/apps/backend/src/worker/objects/org-objects.test.ts index 452e294e4..9dcfe9a74 100644 --- a/apps/backend/src/worker/objects/org-objects.test.ts +++ b/apps/backend/src/worker/objects/org-objects.test.ts @@ -24,7 +24,6 @@ const uuid = () => crypto.randomUUID() const userOf = (id: string) => new CurrentUser.Schema({ id: id as UserId, - organizationId: null, role: "member", email: `${id}@example.com`, isOnboarded: true, @@ -578,7 +577,6 @@ describe("OrgObject: channels", () => { unnamed, new CurrentUser.Schema({ id: world.owner, - organizationId: null, role: "member", email: "owner@example.com", isOnboarded: true, diff --git a/apps/backend/src/worker/objects/org/organization.ts b/apps/backend/src/worker/objects/org/organization.ts index 636efbabf..088ff00a1 100644 --- a/apps/backend/src/worker/objects/org/organization.ts +++ b/apps/backend/src/worker/objects/org/organization.ts @@ -3,7 +3,6 @@ * cascade of an organization delete to its members' `membership_index` and `user_organizations`. */ import { Database, SQLite, schema } from "@hazel/db" -import { CurrentUser } from "@hazel/domain" import { type OrganizationId, OrganizationId as OrganizationIdSchema } from "@hazel/schema" import { Effect, Option } from "effect" import { currentObject } from "../../../lib/current-object" @@ -62,13 +61,11 @@ export const publishOrgRemoval = Effect.fn("OrgObject.publishOrgRemoval")(functi /** * The organization a `listByOrganization`-style call is about: the `OrgObject` it landed in (the - * api Worker routed it by its `organizationId` hint), else the session's active organization. + * api Worker routed it by its `organizationId` hint); none elsewhere. */ export const requestOrganizationId = Effect.gen(function* () { const object = yield* currentObject - if (Option.isSome(object) && object.value.kind === "org") { - return Option.some(OrganizationIdSchema.make(object.value.id)) - } - const user = yield* CurrentUser.Context - return Option.fromNullishOr(user.organizationId) + return Option.isSome(object) && object.value.kind === "org" + ? Option.some(OrganizationIdSchema.make(object.value.id)) + : Option.none() }) diff --git a/apps/web-foldkit/public/images/integrations/t3code.svg b/apps/web-foldkit/public/images/integrations/t3code.svg new file mode 100644 index 000000000..da47f1ba9 --- /dev/null +++ b/apps/web-foldkit/public/images/integrations/t3code.svg @@ -0,0 +1 @@ +T3 Code diff --git a/apps/web-foldkit/src/app/command.ts b/apps/web-foldkit/src/app/command.ts index 1c978d85a..3a1b8b7da 100644 --- a/apps/web-foldkit/src/app/command.ts +++ b/apps/web-foldkit/src/app/command.ts @@ -120,7 +120,6 @@ const fetchCurrentUser = Effect.gen(function* () { email: user.email, avatarUrl: user.avatarUrl ?? null, isOnboarded: user.isOnboarded, - organizationId: user.organizationId ?? null, }, }) }).pipe(catchCommon((reason) => Message.FailedFetchCurrentUser({ reason }))) diff --git a/apps/web-foldkit/src/core/lib/integrations/__data.ts b/apps/web-foldkit/src/core/lib/integrations/__data.ts index 387f7bfb8..e9d968556 100644 --- a/apps/web-foldkit/src/core/lib/integrations/__data.ts +++ b/apps/web-foldkit/src/core/lib/integrations/__data.ts @@ -226,12 +226,12 @@ export const integrations: Integration[] = [ }, { id: "t3code", + logoSrc: "/images/integrations/t3code.svg", name: "T3 Code", description: "Drive T3 Code threads on your own machine from Hazel.", fullDescription: "Link your T3 Code environment to start and steer its threads with /t3 from any conversation. Each member links their own machine and chooses who else in the organization may drive it; every action is posted in the Hazel thread.", - logoDomain: "t3.chat", - logoType: "icon", + logoDomain: "t3.codes", brandColor: "#1C1C1E", category: "developer-tools", connectionType: "environment", diff --git a/apps/web-foldkit/src/page/chat/channel/fixtures.test-support.ts b/apps/web-foldkit/src/page/chat/channel/fixtures.test-support.ts index 22a79199f..b8055cebf 100644 --- a/apps/web-foldkit/src/page/chat/channel/fixtures.test-support.ts +++ b/apps/web-foldkit/src/page/chat/channel/fixtures.test-support.ts @@ -29,7 +29,6 @@ export const shared: Shared = { email: "ada@hazel.sh", avatarUrl: null, isOnboarded: true, - organizationId, }, organization: { id: organizationId, name: "Hazel", slug: "hazel", logoUrl: null }, member: null, diff --git a/apps/web-foldkit/src/page/chat/channel/subscription.ts b/apps/web-foldkit/src/page/chat/channel/subscription.ts index 381f743a7..e99b1cdbc 100644 --- a/apps/web-foldkit/src/page/chat/channel/subscription.ts +++ b/apps/web-foldkit/src/page/chat/channel/subscription.ts @@ -364,7 +364,7 @@ const write = Subscription.make()((en { organizationId: Schema.NullOr(OrganizationId) }, { modelToDependencies: ({ shared }) => ({ - organizationId: shared.currentUser?.organizationId ?? null, + organizationId: shared.organization?.id ?? null, }), dependenciesToStream: ({ organizationId }) => organizationId === null @@ -378,7 +378,7 @@ const write = Subscription.make()((en { organizationId: Schema.NullOr(OrganizationId) }, { modelToDependencies: ({ shared }) => ({ - organizationId: shared.currentUser?.organizationId ?? null, + organizationId: shared.organization?.id ?? null, }), dependenciesToStream: ({ organizationId }) => organizationId === null diff --git a/apps/web-foldkit/src/page/chat/channel/write.ts b/apps/web-foldkit/src/page/chat/channel/write.ts index 4a744e862..835c53acb 100644 --- a/apps/web-foldkit/src/page/chat/channel/write.ts +++ b/apps/web-foldkit/src/page/chat/channel/write.ts @@ -83,7 +83,7 @@ const lastOwn = (messages: ReadonlyArray, currentUserId: string | n /** What a draft needs from the page: user, organization, membership, last own message. */ export const contextFor = (model: Model, which: Which, shared: Shared | null): Draft.Context => ({ currentUserId: model.currentUserId, - organizationId: shared?.currentUser?.organizationId ?? null, + organizationId: shared?.organization?.id ?? null, memberId: which === "channel" ? (model.members?.find((member) => member.userId === model.currentUserId)?.id ?? null) diff --git a/apps/web-foldkit/src/page/home/story.test.ts b/apps/web-foldkit/src/page/home/story.test.ts index 6ef14f4dc..6038318b5 100644 --- a/apps/web-foldkit/src/page/home/story.test.ts +++ b/apps/web-foldkit/src/page/home/story.test.ts @@ -37,7 +37,6 @@ const shared: Shared = { email: "ada@hazel.test", avatarUrl: null, isOnboarded: true, - organizationId, }, organization: { id: organizationId, name: "Hazel", slug: "hazel", logoUrl: null }, member: null, diff --git a/apps/web-foldkit/src/page/my-settings/linked-accounts/story.test.ts b/apps/web-foldkit/src/page/my-settings/linked-accounts/story.test.ts index 8cdc17b89..b2c02fe6a 100644 --- a/apps/web-foldkit/src/page/my-settings/linked-accounts/story.test.ts +++ b/apps/web-foldkit/src/page/my-settings/linked-accounts/story.test.ts @@ -3,7 +3,7 @@ import { routeOfPath } from "../../../route" import { Option } from "effect" import { Command, expectNoOutMessage, expectOutMessage, given, message, model, story } from "foldkit/story" import { describe, expect, test } from "vitest" -import { currentUser, makeShared, organizationId, storyUpdate } from "../../../test/pages-fixtures" +import { makeShared, organizationId, storyUpdate } from "../../../test/pages-fixtures" import { PageOutMessage } from "../../out-message" import { DisconnectDiscord, StartDiscordLink } from "./command" import { Message } from "./message" @@ -63,7 +63,7 @@ describe("link discord", () => { }) test("without an organization nothing starts", () => { - const orphan = makeShared({ currentUser: { ...currentUser, organizationId: null } }) + const orphan = makeShared({ organization: null }) story( storyUpdate(update, orphan), given(initial()), diff --git a/apps/web-foldkit/src/page/my-settings/linked-accounts/subscription.ts b/apps/web-foldkit/src/page/my-settings/linked-accounts/subscription.ts index 0f93e3191..48d33bfb6 100644 --- a/apps/web-foldkit/src/page/my-settings/linked-accounts/subscription.ts +++ b/apps/web-foldkit/src/page/my-settings/linked-accounts/subscription.ts @@ -20,7 +20,7 @@ const discordConnection = Subscription.make, Messag { organizationId: Schema.NullOr(OrganizationId), userId: Schema.NullOr(UserId) }, { modelToDependencies: ({ shared }) => ({ - organizationId: shared.currentUser?.organizationId ?? null, + organizationId: shared.organization?.id ?? null, userId: shared.currentUser?.id ?? null, }), dependenciesToStream: ({ organizationId, userId }) => diff --git a/apps/web-foldkit/src/page/my-settings/linked-accounts/update.ts b/apps/web-foldkit/src/page/my-settings/linked-accounts/update.ts index 2c4b66314..198602ca8 100644 --- a/apps/web-foldkit/src/page/my-settings/linked-accounts/update.ts +++ b/apps/web-foldkit/src/page/my-settings/linked-accounts/update.ts @@ -71,7 +71,7 @@ const unsupported = { } export const update = (model: Model, message: Message, shared: Shared): Return => { - const orgId = shared.currentUser?.organizationId ?? null + const orgId = shared.organization?.id ?? null const isIdle = model.link._tag === "Idle" const toIdle = () => modifyFields(model, { link: () => Link.Idle() }) return Message.match(message, { diff --git a/apps/web-foldkit/src/page/my-settings/notifications/story.test.ts b/apps/web-foldkit/src/page/my-settings/notifications/story.test.ts index 67010b51f..18480a553 100644 --- a/apps/web-foldkit/src/page/my-settings/notifications/story.test.ts +++ b/apps/web-foldkit/src/page/my-settings/notifications/story.test.ts @@ -27,7 +27,6 @@ const shared: Shared = { email: "ada@hazel.test", avatarUrl: null, isOnboarded: true, - organizationId: null, }, organization: null, member: null, diff --git a/apps/web-foldkit/src/page/my-settings/profile/profile.scene.test.ts b/apps/web-foldkit/src/page/my-settings/profile/profile.scene.test.ts index 4f36af8a3..6119696ae 100644 --- a/apps/web-foldkit/src/page/my-settings/profile/profile.scene.test.ts +++ b/apps/web-foldkit/src/page/my-settings/profile/profile.scene.test.ts @@ -32,7 +32,6 @@ const shared: Shared = { email: "ada@hazel.test", avatarUrl: null, isOnboarded: true, - organizationId: null, }, organization: null, member: null, diff --git a/apps/web-foldkit/src/page/my-settings/profile/profile.story.test.ts b/apps/web-foldkit/src/page/my-settings/profile/profile.story.test.ts index fa8165986..b198b650a 100644 --- a/apps/web-foldkit/src/page/my-settings/profile/profile.story.test.ts +++ b/apps/web-foldkit/src/page/my-settings/profile/profile.story.test.ts @@ -35,7 +35,6 @@ const shared: Shared = { email: "ada@hazel.test", avatarUrl: null, isOnboarded: true, - organizationId: null, }, organization: null, member: null, diff --git a/apps/web-foldkit/src/page/onboarding/scene.test.ts b/apps/web-foldkit/src/page/onboarding/scene.test.ts index 887109d34..c1cb2dd2e 100644 --- a/apps/web-foldkit/src/page/onboarding/scene.test.ts +++ b/apps/web-foldkit/src/page/onboarding/scene.test.ts @@ -39,7 +39,6 @@ const shared: Shared = { email: "nora@hazel.test", avatarUrl: null, isOnboarded: false, - organizationId: null, }, organization: null, member: null, diff --git a/apps/web-foldkit/src/page/root/index.ts b/apps/web-foldkit/src/page/root/index.ts index a8fd75327..540ad0f99 100644 --- a/apps/web-foldkit/src/page/root/index.ts +++ b/apps/web-foldkit/src/page/root/index.ts @@ -1,5 +1,5 @@ import { OrganizationId, UserId } from "@hazel/schema" -import { and, eq } from "@tanstack/db" +import { eq } from "@tanstack/db" import { Effect, Option, Schema, Stream } from "effect" import { Command, Submodel, Subscription } from "foldkit" import { modifyFields } from "foldkit/struct" @@ -78,13 +78,10 @@ interface MembershipRow { const subscriptions = Subscription.make, Message>()((entry) => ({ membership: entry( - { userId: Schema.NullOr(UserId), organizationId: Schema.NullOr(OrganizationId) }, + { userId: Schema.NullOr(UserId) }, { - modelToDependencies: ({ shared }) => ({ - userId: shared.currentUser?.id ?? null, - organizationId: shared.currentUser?.organizationId ?? null, - }), - dependenciesToStream: ({ userId, organizationId }) => + modelToDependencies: ({ shared }) => ({ userId: shared.currentUser?.id ?? null }), + dependenciesToStream: ({ userId }) => userId === null ? Stream.empty : liveQueryStream( @@ -94,15 +91,7 @@ const subscriptions = Subscription.make, Message>() .innerJoin({ org: organizationCollection }, ({ member, org }) => eq(member.organizationId, org.id), ) - // With an org in the JWT, only that membership counts. - .where(({ member }) => - organizationId - ? and( - eq(member.userId, userId), - eq(member.organizationId, organizationId), - ) - : eq(member.userId, userId), - ) + .where(({ member }) => eq(member.userId, userId)) .findOne(), (rows) => { const row = rows[0] diff --git a/apps/web-foldkit/src/page/settings/integrations/index/subscription.ts b/apps/web-foldkit/src/page/settings/integrations/index/subscription.ts index 0e0fd10ca..815ba2f39 100644 --- a/apps/web-foldkit/src/page/settings/integrations/index/subscription.ts +++ b/apps/web-foldkit/src/page/settings/integrations/index/subscription.ts @@ -16,9 +16,8 @@ export const subscriptions = Subscription.make, Mes connections: entry( { organizationId: Schema.NullOr(OrganizationId) }, { - // Legacy reads `useAuth().user.organizationId` here. modelToDependencies: ({ shared }) => ({ - organizationId: shared.currentUser?.organizationId ?? null, + organizationId: shared.organization?.id ?? null, }), dependenciesToStream: ({ organizationId }) => organizationId === null @@ -32,7 +31,7 @@ export const subscriptions = Subscription.make, Mes { organizationId: Schema.NullOr(OrganizationId), userId: Schema.NullOr(UserId) }, { modelToDependencies: ({ shared }) => ({ - organizationId: shared.currentUser?.organizationId ?? null, + organizationId: shared.organization?.id ?? null, userId: shared.currentUser?.id ?? null, }), dependenciesToStream: ({ organizationId, userId }) => diff --git a/apps/web-foldkit/src/page/settings/integrations/installed/subscription.ts b/apps/web-foldkit/src/page/settings/integrations/installed/subscription.ts index 3e4fa8171..4475660f3 100644 --- a/apps/web-foldkit/src/page/settings/integrations/installed/subscription.ts +++ b/apps/web-foldkit/src/page/settings/integrations/installed/subscription.ts @@ -12,7 +12,7 @@ export const subscriptions = Subscription.make, Mes { organizationId: Schema.NullOr(OrganizationId) }, { modelToDependencies: ({ shared }) => ({ - organizationId: shared.currentUser?.organizationId ?? null, + organizationId: shared.organization?.id ?? null, }), dependenciesToStream: ({ organizationId }) => organizationId === null diff --git a/apps/web-foldkit/src/page/settings/integrations/marketplace/subscription.ts b/apps/web-foldkit/src/page/settings/integrations/marketplace/subscription.ts index c2a1f9f41..6b3782691 100644 --- a/apps/web-foldkit/src/page/settings/integrations/marketplace/subscription.ts +++ b/apps/web-foldkit/src/page/settings/integrations/marketplace/subscription.ts @@ -25,7 +25,7 @@ const dataSubscriptions = Subscription.make, Messag { organizationId: Schema.NullOr(OrganizationId) }, { modelToDependencies: ({ shared }) => ({ - organizationId: shared.currentUser?.organizationId ?? null, + organizationId: shared.organization?.id ?? null, }), dependenciesToStream: ({ organizationId }) => organizationId === null diff --git a/apps/web-foldkit/src/route.test.ts b/apps/web-foldkit/src/route.test.ts index 1156ed654..fdee22ffa 100644 --- a/apps/web-foldkit/src/route.test.ts +++ b/apps/web-foldkit/src/route.test.ts @@ -172,7 +172,6 @@ describe("redirects", () => { email: "ada@hazel.test", avatarUrl: null, isOnboarded, - organizationId: null, }) it("resolves the root redirect like `_app/index.tsx`", () => { diff --git a/apps/web-foldkit/src/session.ts b/apps/web-foldkit/src/session.ts index 4d5da914d..5e730252a 100644 --- a/apps/web-foldkit/src/session.ts +++ b/apps/web-foldkit/src/session.ts @@ -10,8 +10,6 @@ export const CurrentUser = Schema.Struct({ email: Schema.String, avatarUrl: Schema.NullOr(Schema.String), isOnboarded: Schema.Boolean, - /** `user.me`'s organization, if any (legacy `useAuth().user.organizationId`). */ - organizationId: Schema.NullOr(OrganizationId), }) export type CurrentUser = typeof CurrentUser.Type diff --git a/apps/web-foldkit/src/test/pages-entry-fixtures.ts b/apps/web-foldkit/src/test/pages-entry-fixtures.ts index dc47453c7..67dddc886 100644 --- a/apps/web-foldkit/src/test/pages-entry-fixtures.ts +++ b/apps/web-foldkit/src/test/pages-entry-fixtures.ts @@ -13,7 +13,7 @@ export const newcomer: Shared = makeShared({ orgSlug: null, organization: null, member: null, - currentUser: { ...currentUser, firstName: "Nora", lastName: "Newcomer", isOnboarded: false, organizationId: null }, + currentUser: { ...currentUser, firstName: "Nora", lastName: "Newcomer", isOnboarded: false }, }) /** A membership with a slug: the user was invited into an existing organization. */ diff --git a/apps/web-foldkit/src/test/pages-fixtures.ts b/apps/web-foldkit/src/test/pages-fixtures.ts index 6c07590fc..f4a5b9144 100644 --- a/apps/web-foldkit/src/test/pages-fixtures.ts +++ b/apps/web-foldkit/src/test/pages-fixtures.ts @@ -26,7 +26,6 @@ export const currentUser: CurrentUser = { email: "ada@hazel.test", avatarUrl: null, isOnboarded: true, - organizationId, } export const memberWithRole = (role: Member["role"]): Member => ({ id: memberId, role }) diff --git a/apps/web-foldkit/src/test/root-fixtures.ts b/apps/web-foldkit/src/test/root-fixtures.ts index 4fe64ce0f..e1df9be86 100644 --- a/apps/web-foldkit/src/test/root-fixtures.ts +++ b/apps/web-foldkit/src/test/root-fixtures.ts @@ -24,7 +24,6 @@ export const ada: CurrentUser = { email: "ada@hazel.test", avatarUrl: null, isOnboarded: true, - organizationId: null, } export const hazelOrg: Organization = { diff --git a/packages/domain/src/current-user.ts b/packages/domain/src/current-user.ts index 91ff2aeb0..53c08365d 100644 --- a/packages/domain/src/current-user.ts +++ b/packages/domain/src/current-user.ts @@ -1,13 +1,12 @@ import { HttpApiMiddleware, HttpApiSecurity } from "effect/http-api" import { Context as EffectContext, Schema as S } from "effect" import { UnauthorizedError } from "./errors" -import { OrganizationId, UserId } from "@hazel/schema" +import { UserId } from "@hazel/schema" import { User } from "./models" import { InvalidBearerTokenError, SessionLoadError, SessionNotProvidedError } from "./session-errors" export class Schema extends S.Class("CurrentUserSchema")({ id: UserId, - organizationId: S.NullishOr(OrganizationId), role: S.Literals(["admin", "member", "owner"]), avatarUrl: S.optional(S.String), firstName: S.optional(S.String),