* feat(cloud): add team management page with WorkOS org members Custom team page using WorkOS API for listing members, inviting, removing, and changing roles. Admin-gated write operations with server-side role checks. Uses Effect API layer pattern with typed endpoints and effect-atom for client-side state. Also fixes Sonner toaster to use system theme detection instead of next-themes, removes next-themes dependency. * style: format * chore: remove unused widget token endpoint and getWidgetToken * fix: remove any types from team handler tests * style: format * fix: add @types/node to storage-postgres for tsgo compat * fix: resolve merge conflict in test file * style: format * fix: resolve tsgo typecheck errors in billing and test files * fix: add explicit Plan types for tsgo compatibility * style: format * fix: cast empty array for tsgo type inference
Showing the changes to apps/cloud/src/api.ts (1 of 19 files). Show all
Unified · Split
| @@ -46,6 +46,8 @@ import { · 45 unmodified lines | |||
| 46 | import { WorkOSAuth } from "./auth/workos"; | 46 | import { WorkOSAuth } from "./auth/workos"; |
| 47 | import { DbService } from "./services/db"; | 47 | import { DbService } from "./services/db"; |
| 48 | import { createOrgExecutor } from "./services/executor"; | 48 | import { createOrgExecutor } from "./services/executor"; |
| 49 | import { TeamOrgApi } from "./team/compose"; | ||
| 50 | import { TeamHandlers } from "./team/handlers"; | ||
| 49 | import { server } from "./env"; | 51 | import { server } from "./env"; |
| 50 | 52 | ||
| 51 | // --------------------------------------------------------------------------- | 53 | // --------------------------------------------------------------------------- |
| @@ -90,6 +92,11 @@ const NonProtectedApiLive = HttpApiBuilder.api(NonProtectedApi).pipe( · 38 unmodified lines | |||
| 90 | Layer.provideMerge(SessionAuthLive), | 92 | Layer.provideMerge(SessionAuthLive), |
| 91 | ); | 93 | ); |
| 92 | 94 | ||
| 95 | const TeamApiLive = HttpApiBuilder.api(TeamOrgApi).pipe( | ||
| 96 | Layer.provide(TeamHandlers), | ||
| 97 | Layer.provideMerge(OrgAuthLive), | ||
| 98 | ); | ||
| 99 | |||
| 93 | // --------------------------------------------------------------------------- | 100 | // --------------------------------------------------------------------------- |
| 94 | // Public auth web handler | 101 | // Public auth web handler |
| 95 | // --------------------------------------------------------------------------- | 102 | // --------------------------------------------------------------------------- |
| @@ -113,6 +120,12 @@ const createNonProtectedHandler = () => · 17 unmodified lines | |||
| 113 | { middleware: HttpMiddleware.logger }, | 120 | { middleware: HttpMiddleware.logger }, |
| 114 | ); | 121 | ); |
| 115 | 122 | ||
| 123 | const createTeamHandler = () => | ||
| 124 | HttpApiBuilder.toWebHandler( | ||
| 125 | TeamApiLive.pipe(Layer.provideMerge(SharedServices), Layer.provideMerge(RouterConfig)), | ||
| 126 | { middleware: HttpMiddleware.logger }, | ||
| 127 | ); | ||
| 128 | |||
| 116 | // --------------------------------------------------------------------------- | 129 | // --------------------------------------------------------------------------- |
| 117 | // Protected handler — must be built per-request because the executor varies | 130 | // Protected handler — must be built per-request because the executor varies |
| 118 | // --------------------------------------------------------------------------- | 131 | // --------------------------------------------------------------------------- |
| @@ -158,6 +171,7 @@ const buildProtectedHandler = ( · 39 unmodified lines | |||
| 158 | 171 | ||
| 159 | const isAuthPath = (pathname: string): boolean => pathname.startsWith("/auth/"); | 172 | const isAuthPath = (pathname: string): boolean => pathname.startsWith("/auth/"); |
| 160 | const isAutumnPath = (pathname: string): boolean => pathname.startsWith("/autumn/"); | 173 | const isAutumnPath = (pathname: string): boolean => pathname.startsWith("/autumn/"); |
| 174 | const isTeamPath = (pathname: string): boolean => pathname.startsWith("/team/"); | ||
| 161 | const isExecutionPath = (pathname: string): boolean => | 175 | const isExecutionPath = (pathname: string): boolean => |
| 162 | pathname === "/executions" || /^\/executions\/[^/]+\/resume$/.test(pathname); | 176 | pathname === "/executions" || /^\/executions\/[^/]+\/resume$/.test(pathname); |
| 163 | 177 | ||
| @@ -241,9 +255,22 @@ const handleAutumnRequest = async (request: Request): Promise<Response> => { · 77 unmodified lines | |||
| 241 | ); | 255 | ); |
| 242 | }; | 256 | }; |
| 243 | 257 | ||
| 258 | // --------------------------------------------------------------------------- | ||
| 259 | // Widget token endpoint — returns a WorkOS widget token for the session user | ||
| 260 | // --------------------------------------------------------------------------- | ||
| 261 | |||
| 244 | export const handleApiRequest = async (request: Request): Promise<Response> => { | 262 | export const handleApiRequest = async (request: Request): Promise<Response> => { |
| 245 | const pathname = new URL(request.url).pathname; | 263 | const pathname = new URL(request.url).pathname; |
| 246 | 264 | ||
| 265 | if (isTeamPath(pathname)) { | ||
| 266 | const handler = createTeamHandler(); | ||
| 267 | try { | ||
| 268 | return await handler.handler(request); | ||
| 269 | } finally { | ||
| 270 | await handler.dispose(); | ||
| 271 | } | ||
| 272 | } | ||
| 273 | |||
| 247 | if (isAutumnPath(pathname)) { | 274 | if (isAutumnPath(pathname)) { |
| 248 | return handleAutumnRequest(request); | 275 | return handleAutumnRequest(request); |
| 249 | } | 276 | } |