Unified · Split
| @@ -109,6 +109,7 @@ describe("ApiKeyService.WorkOS", () => { · 108 unmodified lines | |||
| 109 | stubWorkOS({ | 109 | stubWorkOS({ |
| 110 | listUserApiKeys: () => | 110 | listUserApiKeys: () => |
| 111 | Effect.succeed({ | 111 | Effect.succeed({ |
| 112 | object: "list" as const, | ||
| 112 | data: [ | 113 | data: [ |
| 113 | { | 114 | { |
| 114 | id: "api_key_listed", | 115 | id: "api_key_listed", |
| @@ -124,6 +125,10 @@ describe("ApiKeyService.WorkOS", () => { · 9 unmodified lines | |||
| 124 | }, | 125 | }, |
| 125 | }, | 126 | }, |
| 126 | ], | 127 | ], |
| 128 | listMetadata: { | ||
| 129 | before: null, | ||
| 130 | after: null, | ||
| 131 | }, | ||
| 127 | }), | 132 | }), |
| 128 | createUserApiKey: () => | 133 | createUserApiKey: () => |
| 129 | Effect.succeed({ | 134 | Effect.succeed({ |
| @@ -0,0 +1,98 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | ||
| 2 | |||
| 3 | import { collectRawWorkOSList, collectWorkOSList } from "./workos"; | ||
| 4 | |||
| 5 | describe("collectWorkOSList", () => { | ||
| 6 | it("collects memberships beyond the first WorkOS page", async () => { | ||
| 7 | const autoPaginationCalls: string[] = []; | ||
| 8 | |||
| 9 | const response = await collectWorkOSList({ | ||
| 10 | object: "list", | ||
| 11 | data: [{ id: "om_first_page" }], | ||
| 12 | listMetadata: { | ||
| 13 | before: null, | ||
| 14 | after: "om_next_page", | ||
| 15 | }, | ||
| 16 | autoPagination: async () => { | ||
| 17 | autoPaginationCalls.push("called"); | ||
| 18 | return [{ id: "om_first_page" }, { id: "om_second_page" }]; | ||
| 19 | }, | ||
| 20 | }); | ||
| 21 | |||
| 22 | expect(response.data).toEqual([{ id: "om_first_page" }, { id: "om_second_page" }]); | ||
| 23 | expect(response.listMetadata).toEqual({ before: null, after: null }); | ||
| 24 | expect(autoPaginationCalls).toEqual(["called"]); | ||
| 25 | }); | ||
| 26 | |||
| 27 | it("keeps the first page when WorkOS reports no next page", async () => { | ||
| 28 | let autoPaginationCalls = 0; | ||
| 29 | |||
| 30 | const response = await collectWorkOSList({ | ||
| 31 | object: "list", | ||
| 32 | data: [{ id: "om_only_page" }], | ||
| 33 | listMetadata: { | ||
| 34 | before: null, | ||
| 35 | after: null, | ||
| 36 | }, | ||
| 37 | autoPagination: async () => { | ||
| 38 | autoPaginationCalls += 1; | ||
| 39 | return [{ id: "om_unexpected_page" }]; | ||
| 40 | }, | ||
| 41 | }); | ||
| 42 | |||
| 43 | expect(response.data).toEqual([{ id: "om_only_page" }]); | ||
| 44 | expect(response.listMetadata).toEqual({ before: null, after: null }); | ||
| 45 | expect(autoPaginationCalls).toBe(0); | ||
| 46 | }); | ||
| 47 | }); | ||
| 48 | |||
| 49 | describe("collectRawWorkOSList", () => { | ||
| 50 | it("collects raw WorkOS lists using snake-case cursors", async () => { | ||
| 51 | const requestedCursors: Array<string | undefined> = []; | ||
| 52 | |||
| 53 | const response = await collectRawWorkOSList(async (after) => { | ||
| 54 | requestedCursors.push(after); | ||
| 55 | return after | ||
| 56 | ? { | ||
| 57 | data: [{ id: "api_key_second_page" }], | ||
| 58 | list_metadata: { | ||
| 59 | before: null, | ||
| 60 | after: null, | ||
| 61 | }, | ||
| 62 | } | ||
| 63 | : { | ||
| 64 | data: [{ id: "api_key_first_page" }], | ||
| 65 | list_metadata: { | ||
| 66 | before: null, | ||
| 67 | after: "api_key_second_page", | ||
| 68 | }, | ||
| 69 | }; | ||
| 70 | }); | ||
| 71 | |||
| 72 | expect(response.data).toEqual([{ id: "api_key_first_page" }, { id: "api_key_second_page" }]); | ||
| 73 | expect(response.listMetadata).toEqual({ before: null, after: null }); | ||
| 74 | expect(requestedCursors).toEqual([undefined, "api_key_second_page"]); | ||
| 75 | }); | ||
| 76 | |||
| 77 | it("collects raw WorkOS lists using camel-case cursors", async () => { | ||
| 78 | const response = await collectRawWorkOSList(async (after) => | ||
| 79 | after | ||
| 80 | ? { | ||
| 81 | data: [{ id: "second" }], | ||
| 82 | listMetadata: { | ||
| 83 | before: null, | ||
| 84 | after: null, | ||
| 85 | }, | ||
| 86 | } | ||
| 87 | : { | ||
| 88 | data: [{ id: "first" }], | ||
| 89 | listMetadata: { | ||
| 90 | before: null, | ||
| 91 | after: "second", | ||
| 92 | }, | ||
| 93 | }, | ||
| 94 | ); | ||
| 95 | |||
| 96 | expect(response.data).toEqual([{ id: "first" }, { id: "second" }]); | ||
| 97 | }); | ||
| 98 | }); | ||
| @@ -3,7 +3,7 @@ · 2 unmodified lines | |||
| 3 | // --------------------------------------------------------------------------- | 3 | // --------------------------------------------------------------------------- |
| 4 | 4 | ||
| 5 | import { env } from "cloudflare:workers"; | 5 | import { env } from "cloudflare:workers"; |
| 6 | import { Context, Data, Effect, Layer } from "effect"; | 6 | import { Context, Data, Effect, Layer, Option, Schema } from "effect"; |
| 7 | import { GeneratePortalLinkIntent, WorkOS } from "@workos-inc/node/worker"; | 7 | import { GeneratePortalLinkIntent, WorkOS } from "@workos-inc/node/worker"; |
| 8 | import { WorkOSError, tryPromiseService, withServiceLogging } from "./errors"; | 8 | import { WorkOSError, tryPromiseService, withServiceLogging } from "./errors"; |
| 9 | 9 | ||
| @@ -24,6 +24,88 @@ type RawWorkOS = WorkOS & { · 14 unmodified lines | |||
| 24 | ) => Promise<{ readonly data: unknown }>; | 24 | ) => Promise<{ readonly data: unknown }>; |
| 25 | }; | 25 | }; |
| 26 | 26 | ||
| 27 | type WorkOSListMetadata = { | ||
| 28 | readonly before?: string | null; | ||
| 29 | readonly after?: string | null; | ||
| 30 | }; | ||
| 31 | |||
| 32 | type WorkOSAutoPaginatable<Resource> = { | ||
| 33 | readonly object: "list"; | ||
| 34 | readonly data: Resource[]; | ||
| 35 | readonly listMetadata: WorkOSListMetadata; | ||
| 36 | readonly autoPagination: () => Promise<Resource[]>; | ||
| 37 | }; | ||
| 38 | |||
| 39 | export type WorkOSCollectedList<Resource> = { | ||
| 40 | readonly object: "list"; | ||
| 41 | readonly data: Resource[]; | ||
| 42 | readonly listMetadata: { | ||
| 43 | readonly before: string | null; | ||
| 44 | readonly after: string | null; | ||
| 45 | }; | ||
| 46 | }; | ||
| 47 | |||
| 48 | const RawWorkOSListMetadata = Schema.Struct({ | ||
| 49 | before: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 50 | after: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 51 | }); | ||
| 52 | |||
| 53 | const RawWorkOSListResponse = Schema.Struct({ | ||
| 54 | data: Schema.Array(Schema.Unknown), | ||
| 55 | listMetadata: Schema.optional(RawWorkOSListMetadata), | ||
| 56 | list_metadata: Schema.optional(RawWorkOSListMetadata), | ||
| 57 | }); | ||
| 58 | |||
| 59 | const decodeRawWorkOSListResponse = Schema.decodeUnknownOption(RawWorkOSListResponse); | ||
| 60 | |||
| 61 | const completedListMetadata = { | ||
| 62 | before: null, | ||
| 63 | after: null, | ||
| 64 | } as const; | ||
| 65 | |||
| 66 | const nextCursorFromRawList = (response: typeof RawWorkOSListResponse.Type): string | null => | ||
| 67 | response.listMetadata?.after ?? response.list_metadata?.after ?? null; | ||
| 68 | |||
| 69 | export const collectWorkOSList = async <Resource>( | ||
| 70 | response: WorkOSAutoPaginatable<Resource>, | ||
| 71 | ): Promise<WorkOSCollectedList<Resource>> => { | ||
| 72 | const data = response.listMetadata.after ? await response.autoPagination() : response.data; | ||
| 73 | return { | ||
| 74 | object: "list", | ||
| 75 | data, | ||
| 76 | listMetadata: completedListMetadata, | ||
| 77 | }; | ||
| 78 | }; | ||
| 79 | |||
| 80 | export const collectRawWorkOSList = async ( | ||
| 81 | loadPage: (after?: string) => Promise<unknown>, | ||
| 82 | ): Promise<WorkOSCollectedList<unknown>> => { | ||
| 83 | const first = Option.getOrNull(decodeRawWorkOSListResponse(await loadPage())); | ||
| 84 | if (!first) { | ||
| 85 | return { | ||
| 86 | object: "list", | ||
| 87 | data: [], | ||
| 88 | listMetadata: completedListMetadata, | ||
| 89 | }; | ||
| 90 | } | ||
| 91 | |||
| 92 | const data = [...first.data]; | ||
| 93 | let after = nextCursorFromRawList(first); | ||
| 94 | |||
| 95 | while (after) { | ||
| 96 | const next = Option.getOrNull(decodeRawWorkOSListResponse(await loadPage(after))); | ||
| 97 | if (!next) break; | ||
| 98 | data.push(...next.data); | ||
| 99 | after = nextCursorFromRawList(next); | ||
| 100 | } | ||
| 101 | |||
| 102 | return { | ||
| 103 | object: "list", | ||
| 104 | data, | ||
| 105 | listMetadata: completedListMetadata, | ||
| 106 | }; | ||
| 107 | }; | ||
| 108 | |||
| 27 | class WorkOSAuthConfigurationError extends Data.TaggedError("WorkOSAuthConfigurationError")<{ | 109 | class WorkOSAuthConfigurationError extends Data.TaggedError("WorkOSAuthConfigurationError")<{ |
| 28 | readonly message: string; | 110 | readonly message: string; |
| 29 | }> {} | 111 | }> {} |
| @@ -132,11 +214,13 @@ const make = Effect.gen(function* () { · 102 unmodified lines | |||
| 132 | 214 | ||
| 133 | /** List organization memberships for a user. */ | 215 | /** List organization memberships for a user. */ |
| 134 | listUserMemberships: (userId: string) => | 216 | listUserMemberships: (userId: string) => |
| 135 | use((wos) => | 217 | use(async (wos) => |
| 136 | wos.userManagement.listOrganizationMemberships({ | 218 | collectWorkOSList( |
| 137 | userId, | 219 | await wos.userManagement.listOrganizationMemberships({ |
| 138 | statuses: ["active", "pending"], | 220 | userId, |
| 139 | }), | 221 | statuses: ["active", "pending"], |
| 222 | }), | ||
| 223 | ), | ||
| 140 | ), | 224 | ), |
| 141 | 225 | ||
| 142 | /** | 226 | /** |
| @@ -183,10 +267,16 @@ const make = Effect.gen(function* () { · 40 unmodified lines | |||
| 183 | listUserApiKeys: (userId: string, organizationId: string) => | 267 | listUserApiKeys: (userId: string, organizationId: string) => |
| 184 | use(async (wos) => { | 268 | use(async (wos) => { |
| 185 | const raw = wos as RawWorkOS; | 269 | const raw = wos as RawWorkOS; |
| 186 | const response = await raw.get(`/user_management/users/${userId}/api_keys`, { | 270 | return collectRawWorkOSList(async (after) => { |
| 187 | query: { organization_id: organizationId }, | 271 | const response = await raw.get(`/user_management/users/${userId}/api_keys`, { |
| 272 | query: { | ||
| 273 | organization_id: organizationId, | ||
| 274 | limit: 100, | ||
| 275 | ...(after ? { after } : {}), | ||
| 276 | }, | ||
| 277 | }); | ||
| 278 | return response.data; | ||
| 188 | }); | 279 | }); |
| 189 | return response.data; | ||
| 190 | }), | 280 | }), |
| 191 | 281 | ||
| 192 | createUserApiKey: (params: { userId: string; organizationId: string; name: string }) => | 282 | createUserApiKey: (params: { userId: string; organizationId: string; name: string }) => |
| @@ -203,12 +293,25 @@ const make = Effect.gen(function* () { · 10 unmodified lines | |||
| 203 | 293 | ||
| 204 | /** List organization memberships with user details. */ | 294 | /** List organization memberships with user details. */ |
| 205 | listOrgMembers: (organizationId: string) => | 295 | listOrgMembers: (organizationId: string) => |
| 206 | use((wos) => | 296 | use(async (wos) => |
| 207 | wos.userManagement.listOrganizationMemberships({ | 297 | collectWorkOSList( |
| 298 | await wos.userManagement.listOrganizationMemberships({ | ||
| 299 | organizationId, | ||
| 300 | statuses: ["active", "pending"], | ||
| 301 | }), | ||
| 302 | ), | ||
| 303 | ), | ||
| 304 | |||
| 305 | /** Get a user's membership in an organization. */ | ||
| 306 | getUserOrgMembership: (organizationId: string, userId: string) => | ||
| 307 | use(async (wos) => { | ||
| 308 | const response = await wos.userManagement.listOrganizationMemberships({ | ||
| 208 | organizationId, | 309 | organizationId, |
| 310 | userId, | ||
| 209 | statuses: ["active", "pending"], | 311 | statuses: ["active", "pending"], |
| 210 | }), | 312 | }); |
| 211 | ), | 313 | return response.data[0] ?? null; |
| 314 | }), | ||
| 212 | 315 | ||
| 213 | /** Get a user by ID. */ | 316 | /** Get a user by ID. */ |
| 214 | getUser: (userId: string) => use((wos) => wos.userManagement.getUser(userId)), | 317 | getUser: (userId: string) => use((wos) => wos.userManagement.getUser(userId)), |
| @@ -229,7 +332,13 @@ const make = Effect.gen(function* () { · 14 unmodified lines | |||
| 229 | * API level, so we filter after. | 332 | * API level, so we filter after. |
| 230 | */ | 333 | */ |
| 231 | listPendingInvitations: (organizationId: string) => | 334 | listPendingInvitations: (organizationId: string) => |
| 232 | use((wos) => wos.userManagement.listInvitations({ organizationId })).pipe( | 335 | use(async (wos) => |
| 336 | collectWorkOSList( | ||
| 337 | await wos.userManagement.listInvitations({ | ||
| 338 | organizationId, | ||
| 339 | }), | ||
| 340 | ), | ||
| 341 | ).pipe( | ||
| 233 | Effect.map((response) => ({ | 342 | Effect.map((response) => ({ |
| 234 | ...response, | 343 | ...response, |
| 235 | data: response.data.filter((i) => i.state === "pending"), | 344 | data: response.data.filter((i) => i.state === "pending"), |
| @@ -238,7 +347,13 @@ const make = Effect.gen(function* () { · 2 unmodified lines | |||
| 238 | 347 | ||
| 239 | /** List invitations for an email address (across all orgs). */ | 348 | /** List invitations for an email address (across all orgs). */ |
| 240 | listInvitationsByEmail: (email: string) => | 349 | listInvitationsByEmail: (email: string) => |
| 241 | use((wos) => wos.userManagement.listInvitations({ email })), | 350 | use(async (wos) => |
| 351 | collectWorkOSList( | ||
| 352 | await wos.userManagement.listInvitations({ | ||
| 353 | email, | ||
| 354 | }), | ||
| 355 | ), | ||
| 356 | ), | ||
| 242 | 357 | ||
| 243 | /** Accept an invitation; returns the (now accepted) invitation. */ | 358 | /** Accept an invitation; returns the (now accepted) invitation. */ |
| 244 | acceptInvitation: (invitationId: string) => | 359 | acceptInvitation: (invitationId: string) => |
| @@ -14,6 +14,7 @@ type StubFn = (...args: never[]) => Effect.Effect<any>; · 13 unmodified lines | |||
| 14 | 14 | ||
| 15 | type StubOverrides = { | 15 | type StubOverrides = { |
| 16 | listOrgMembers?: StubFn; | 16 | listOrgMembers?: StubFn; |
| 17 | getUserOrgMembership?: StubFn; | ||
| 17 | getUser?: StubFn; | 18 | getUser?: StubFn; |
| 18 | sendInvitation?: StubFn; | 19 | sendInvitation?: StubFn; |
| 19 | deleteOrgMembership?: StubFn; | 20 | deleteOrgMembership?: StubFn; |
| @@ -122,8 +123,7 @@ const fakeRoles: FakeRole[] = [ · 102 unmodified lines | |||
| 122 | const requireAdmin = Effect.gen(function* () { | 123 | const requireAdmin = Effect.gen(function* () { |
| 123 | const auth = yield* AuthContext; | 124 | const auth = yield* AuthContext; |
| 124 | const workos = yield* WorkOSAuth; | 125 | const workos = yield* WorkOSAuth; |
| 125 | const memberships = yield* workos.listOrgMembers(auth.organizationId); | 126 | const current = yield* workos.getUserOrgMembership(auth.organizationId, auth.accountId); |
| 126 | const current = memberships.data.find((m: FakeMembership) => m.userId === auth.accountId); | ||
| 127 | if (!current || current.role?.slug !== "admin") { | 127 | if (!current || current.role?.slug !== "admin") { |
| 128 | return yield* new Forbidden(); | 128 | return yield* new Forbidden(); |
| 129 | } | 129 | } |
| @@ -136,6 +136,11 @@ const withMembers: StubOverrides = { · 6 unmodified lines | |||
| 136 | listOrgMembers: () => Effect.succeed({ data: fakeMemberships }), | 136 | listOrgMembers: () => Effect.succeed({ data: fakeMemberships }), |
| 137 | }; | 137 | }; |
| 138 | 138 | ||
| 139 | const withCurrentMembership: StubOverrides = { | ||
| 140 | getUserOrgMembership: (_organizationId: string, userId: string) => | ||
| 141 | Effect.succeed(fakeMemberships.find((m) => m.userId === userId) ?? null), | ||
| 142 | }; | ||
| 143 | |||
| 139 | // --------------------------------------------------------------------------- | 144 | // --------------------------------------------------------------------------- |
| 140 | // Tests | 145 | // Tests |
| 141 | // --------------------------------------------------------------------------- | 146 | // --------------------------------------------------------------------------- |
| @@ -205,14 +210,14 @@ describe("Org handlers", () => { · 63 unmodified lines | |||
| 205 | 210 | ||
| 206 | describe("requireAdmin", () => { | 211 | describe("requireAdmin", () => { |
| 207 | it.effect("passes for admin user", () => | 212 | it.effect("passes for admin user", () => |
| 208 | requireAdmin.pipe(Effect.provide(provide(adminAuth, withMembers))), | 213 | requireAdmin.pipe(Effect.provide(provide(adminAuth, withCurrentMembership))), |
| 209 | ); | 214 | ); |
| 210 | 215 | ||
| 211 | it.effect("rejects non-admin with Forbidden", () => | 216 | it.effect("rejects non-admin with Forbidden", () => |
| 212 | Effect.gen(function* () { | 217 | Effect.gen(function* () { |
| 213 | const error = yield* Effect.flip(requireAdmin); | 218 | const error = yield* Effect.flip(requireAdmin); |
| 214 | expect(error).toBeInstanceOf(Forbidden); | 219 | expect(error).toBeInstanceOf(Forbidden); |
| 215 | }).pipe(Effect.provide(provide(memberAuth, withMembers))), | 220 | }).pipe(Effect.provide(provide(memberAuth, withCurrentMembership))), |
| 216 | ); | 221 | ); |
| 217 | }); | 222 | }); |
| 218 | 223 | ||
| @@ -231,7 +236,7 @@ describe("Org handlers", () => { · 12 unmodified lines | |||
| 231 | }).pipe( | 236 | }).pipe( |
| 232 | Effect.provide( | 237 | Effect.provide( |
| 233 | provide(adminAuth, { | 238 | provide(adminAuth, { |
| 234 | ...withMembers, | 239 | ...withCurrentMembership, |
| 235 | sendInvitation: (p: { email: string }) => | 240 | sendInvitation: (p: { email: string }) => |
| 236 | Effect.succeed({ id: "inv_1", email: p.email }), | 241 | Effect.succeed({ id: "inv_1", email: p.email }), |
| 237 | }), | 242 | }), |
| @@ -252,7 +257,7 @@ describe("Org handlers", () => { · 14 unmodified lines | |||
| 252 | }), | 257 | }), |
| 253 | ); | 258 | ); |
| 254 | expect(error).toBeInstanceOf(Forbidden); | 259 | expect(error).toBeInstanceOf(Forbidden); |
| 255 | }).pipe(Effect.provide(provide(memberAuth, withMembers))), | 260 | }).pipe(Effect.provide(provide(memberAuth, withCurrentMembership))), |
| 256 | ); | 261 | ); |
| 257 | }); | 262 | }); |
| 258 | 263 | ||
| @@ -265,7 +270,7 @@ describe("Org handlers", () => { · 6 unmodified lines | |||
| 265 | }).pipe( | 270 | }).pipe( |
| 266 | Effect.provide( | 271 | Effect.provide( |
| 267 | provide(adminAuth, { | 272 | provide(adminAuth, { |
| 268 | ...withMembers, | 273 | ...withCurrentMembership, |
| 269 | deleteOrgMembership: () => Effect.void, | 274 | deleteOrgMembership: () => Effect.void, |
| 270 | }), | 275 | }), |
| 271 | ), | 276 | ), |
| @@ -282,7 +287,7 @@ describe("Org handlers", () => { · 10 unmodified lines | |||
| 282 | }), | 287 | }), |
| 283 | ); | 288 | ); |
| 284 | expect(error).toBeInstanceOf(Forbidden); | 289 | expect(error).toBeInstanceOf(Forbidden); |
| 285 | }).pipe(Effect.provide(provide(memberAuth, withMembers))), | 290 | }).pipe(Effect.provide(provide(memberAuth, withCurrentMembership))), |
| 286 | ); | 291 | ); |
| 287 | }); | 292 | }); |
| 288 | 293 | ||
| @@ -295,7 +300,7 @@ describe("Org handlers", () => { · 6 unmodified lines | |||
| 295 | }).pipe( | 300 | }).pipe( |
| 296 | Effect.provide( | 301 | Effect.provide( |
| 297 | provide(adminAuth, { | 302 | provide(adminAuth, { |
| 298 | ...withMembers, | 303 | ...withCurrentMembership, |
| 299 | updateOrgMembershipRole: () => Effect.void, | 304 | updateOrgMembershipRole: () => Effect.void, |
| 300 | }), | 305 | }), |
| 301 | ), | 306 | ), |
| @@ -312,7 +317,7 @@ describe("Org handlers", () => { · 10 unmodified lines | |||
| 312 | }), | 317 | }), |
| 313 | ); | 318 | ); |
| 314 | expect(error).toBeInstanceOf(Forbidden); | 319 | expect(error).toBeInstanceOf(Forbidden); |
| 315 | }).pipe(Effect.provide(provide(memberAuth, withMembers))), | 320 | }).pipe(Effect.provide(provide(memberAuth, withCurrentMembership))), |
| 316 | ); | 321 | ); |
| 317 | }); | 322 | }); |
| 318 | }); | 323 | }); |
| @@ -13,8 +13,7 @@ import { getMemberLimitForPlan, selectActiveMemberLimitPlan } from "./member-lim · 12 unmodified lines | |||
| 13 | const requireAdmin = Effect.gen(function* () { | 13 | const requireAdmin = Effect.gen(function* () { |
| 14 | const auth = yield* AuthContext; | 14 | const auth = yield* AuthContext; |
| 15 | const workos = yield* WorkOSAuth; | 15 | const workos = yield* WorkOSAuth; |
| 16 | const memberships = yield* workos.listOrgMembers(auth.organizationId); | 16 | const currentMembership = yield* workos.getUserOrgMembership(auth.organizationId, auth.accountId); |
| 17 | const currentMembership = memberships.data.find((m) => m.userId === auth.accountId); | ||
| 18 | if (!currentMembership || currentMembership.role?.slug !== "admin") { | 17 | if (!currentMembership || currentMembership.role?.slug !== "admin") { |
| 19 | return yield* new Forbidden(); | 18 | return yield* new Forbidden(); |
| 20 | } | 19 | } |