Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/cloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"jose": "^5.6.3",
"postgres": "^3.4.9",
"react": "catalog:",
"react-dom": "catalog:"
"react-dom": "catalog:",
"sonner": "^2.0.7"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.14.3",
Expand Down
27 changes: 27 additions & 0 deletions apps/cloud/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import {
import { WorkOSAuth } from "./auth/workos";
import { DbService } from "./services/db";
import { createOrgExecutor } from "./services/executor";
import { TeamOrgApi } from "./team/compose";
import { TeamHandlers } from "./team/handlers";
import { server } from "./env";

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -90,6 +92,11 @@ const NonProtectedApiLive = HttpApiBuilder.api(NonProtectedApi).pipe(
Layer.provideMerge(SessionAuthLive),
);

const TeamApiLive = HttpApiBuilder.api(TeamOrgApi).pipe(
Layer.provide(TeamHandlers),
Layer.provideMerge(OrgAuthLive),
);

// ---------------------------------------------------------------------------
// Public auth web handler
// ---------------------------------------------------------------------------
Expand All @@ -113,6 +120,12 @@ const createNonProtectedHandler = () =>
{ middleware: HttpMiddleware.logger },
);

const createTeamHandler = () =>
HttpApiBuilder.toWebHandler(
TeamApiLive.pipe(Layer.provideMerge(SharedServices), Layer.provideMerge(RouterConfig)),
{ middleware: HttpMiddleware.logger },
);

// ---------------------------------------------------------------------------
// Protected handler — must be built per-request because the executor varies
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -158,6 +171,7 @@ const buildProtectedHandler = (

const isAuthPath = (pathname: string): boolean => pathname.startsWith("/auth/");
const isAutumnPath = (pathname: string): boolean => pathname.startsWith("/autumn/");
const isTeamPath = (pathname: string): boolean => pathname.startsWith("/team/");
const isExecutionPath = (pathname: string): boolean =>
pathname === "/executions" || /^\/executions\/[^/]+\/resume$/.test(pathname);

Expand Down Expand Up @@ -241,9 +255,22 @@ const handleAutumnRequest = async (request: Request): Promise<Response> => {
);
};

// ---------------------------------------------------------------------------
// Widget token endpoint — returns a WorkOS widget token for the session user
// ---------------------------------------------------------------------------

export const handleApiRequest = async (request: Request): Promise<Response> => {
const pathname = new URL(request.url).pathname;

if (isTeamPath(pathname)) {
const handler = createTeamHandler();
try {
return await handler.handler(request);
} finally {
await handler.dispose();
}
}

if (isAutumnPath(pathname)) {
return handleAutumnRequest(request);
}
Expand Down
40 changes: 39 additions & 1 deletion apps/cloud/src/auth/workos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,48 @@ const make = Effect.gen(function* () {
if (!sessionData) return null;
return yield* authenticateSealedSession(sessionData);
}),

/** List organization memberships with user details. */
listOrgMembers: (organizationId: string) =>
use((wos) =>
wos.userManagement.listOrganizationMemberships({
organizationId,
statuses: ["active", "pending"],
}),
),

/** Get a user by ID. */
getUser: (userId: string) => use((wos) => wos.userManagement.getUser(userId)),

/** Send an organization invitation. */
sendInvitation: (params: { email: string; organizationId: string; roleSlug?: string }) =>
use((wos) =>
wos.userManagement.sendInvitation({
email: params.email,
organizationId: params.organizationId,
roleSlug: params.roleSlug,
}),
),

/** Remove an organization membership. */
deleteOrgMembership: (membershipId: string) =>
use((wos) => wos.userManagement.deleteOrganizationMembership(membershipId)),

/** Get the role for a membership. */
getOrgMembership: (membershipId: string) =>
use((wos) => wos.userManagement.getOrganizationMembership(membershipId)),

/** Update a membership's role. */
updateOrgMembershipRole: (membershipId: string, roleSlug: string) =>
use((wos) => wos.userManagement.updateOrganizationMembership(membershipId, { roleSlug })),

/** List available roles for an organization. */
listOrgRoles: (organizationId: string) =>
use((wos) => wos.organizations.listOrganizationRoles({ organizationId })),
};
});

type WorkOSAuthService = Effect.Effect.Success<typeof make>;
export type WorkOSAuthService = Effect.Effect.Success<typeof make>;

export class WorkOSAuth extends Context.Tag("@executor/cloud/WorkOSAuth")<
WorkOSAuth,
Expand Down
21 changes: 21 additions & 0 deletions apps/cloud/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import { Route as rootRouteImport } from "./routes/__root";
import { Route as ToolsRouteImport } from "./routes/tools";
import { Route as TeamRouteImport } from "./routes/team";
import { Route as SecretsRouteImport } from "./routes/secrets";
import { Route as BillingRouteImport } from "./routes/billing";
import { Route as IndexRouteImport } from "./routes/index";
Expand All @@ -22,6 +23,11 @@ const ToolsRoute = ToolsRouteImport.update({
path: "/tools",
getParentRoute: () => rootRouteImport,
} as any);
const TeamRoute = TeamRouteImport.update({
id: "/team",
path: "/team",
getParentRoute: () => rootRouteImport,
} as any);
const SecretsRoute = SecretsRouteImport.update({
id: "/secrets",
path: "/secrets",
Expand Down Expand Up @@ -57,6 +63,7 @@ export interface FileRoutesByFullPath {
"/": typeof IndexRoute;
"/billing": typeof BillingRoute;
"/secrets": typeof SecretsRoute;
"/team": typeof TeamRoute;
"/tools": typeof ToolsRoute;
"/billing/plans": typeof BillingPlansRoute;
"/sources/$namespace": typeof SourcesNamespaceRoute;
Expand All @@ -66,6 +73,7 @@ export interface FileRoutesByTo {
"/": typeof IndexRoute;
"/billing": typeof BillingRoute;
"/secrets": typeof SecretsRoute;
"/team": typeof TeamRoute;
"/tools": typeof ToolsRoute;
"/billing/plans": typeof BillingPlansRoute;
"/sources/$namespace": typeof SourcesNamespaceRoute;
Expand All @@ -76,6 +84,7 @@ export interface FileRoutesById {
"/": typeof IndexRoute;
"/billing": typeof BillingRoute;
"/secrets": typeof SecretsRoute;
"/team": typeof TeamRoute;
"/tools": typeof ToolsRoute;
"/billing_/plans": typeof BillingPlansRoute;
"/sources/$namespace": typeof SourcesNamespaceRoute;
Expand All @@ -87,6 +96,7 @@ export interface FileRouteTypes {
| "/"
| "/billing"
| "/secrets"
| "/team"
| "/tools"
| "/billing/plans"
| "/sources/$namespace"
Expand All @@ -96,6 +106,7 @@ export interface FileRouteTypes {
| "/"
| "/billing"
| "/secrets"
| "/team"
| "/tools"
| "/billing/plans"
| "/sources/$namespace"
Expand All @@ -105,6 +116,7 @@ export interface FileRouteTypes {
| "/"
| "/billing"
| "/secrets"
| "/team"
| "/tools"
| "/billing_/plans"
| "/sources/$namespace"
Expand All @@ -115,6 +127,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute;
BillingRoute: typeof BillingRoute;
SecretsRoute: typeof SecretsRoute;
TeamRoute: typeof TeamRoute;
ToolsRoute: typeof ToolsRoute;
BillingPlansRoute: typeof BillingPlansRoute;
SourcesNamespaceRoute: typeof SourcesNamespaceRoute;
Expand All @@ -130,6 +143,13 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof ToolsRouteImport;
parentRoute: typeof rootRouteImport;
};
"/team": {
id: "/team";
path: "/team";
fullPath: "/team";
preLoaderRoute: typeof TeamRouteImport;
parentRoute: typeof rootRouteImport;
};
"/secrets": {
id: "/secrets";
path: "/secrets";
Expand Down Expand Up @@ -179,6 +199,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
BillingRoute: BillingRoute,
SecretsRoute: SecretsRoute,
TeamRoute: TeamRoute,
ToolsRoute: ToolsRoute,
BillingPlansRoute: BillingPlansRoute,
SourcesNamespaceRoute: SourcesNamespaceRoute,
Expand Down
2 changes: 2 additions & 0 deletions apps/cloud/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";
import { AutumnProvider } from "autumn-js/react";
import { ExecutorProvider } from "@executor/react/api/provider";
import { Toaster } from "@executor/react/components/sonner";
import { AuthProvider, useAuth } from "../web/auth";
import { LoginPage } from "../web/pages/login";
import { Shell } from "../web/shell";
Expand Down Expand Up @@ -70,6 +71,7 @@ function AuthGate() {
<AutumnProvider pathPrefix="/api/autumn">
<ExecutorProvider>
<Shell />
<Toaster />
</ExecutorProvider>
</AutumnProvider>
);
Expand Down
8 changes: 5 additions & 3 deletions apps/cloud/src/routes/billing.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useCustomer, useListPlans } from "autumn-js/react";

type Plan = NonNullable<ReturnType<typeof useListPlans>["data"]>[number];

export const Route = createFileRoute("/billing")({
component: BillingPage,
});
Expand Down Expand Up @@ -28,11 +30,11 @@ function BillingPage() {
);
}

// Find current plan via customerEligibility from useListPlans
const activePlan = (plans ?? []).find(
const allPlans: Plan[] = plans ?? [];
const activePlan = allPlans.find(
(p) => p.customerEligibility?.status === "active" && p.id !== "free",
);
const scheduledPlan = (plans ?? []).find(
const scheduledPlan = allPlans.find(
(p) => p.customerEligibility?.status === "scheduled" && p.id !== "free",
);
const isCanceling = activePlan?.customerEligibility?.canceling ?? false;
Expand Down
6 changes: 5 additions & 1 deletion apps/cloud/src/routes/billing_.plans.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { useState } from "react";
import { createFileRoute, Link } from "@tanstack/react-router";
import { useCustomer, useListPlans } from "autumn-js/react";

type Plan = NonNullable<ReturnType<typeof useListPlans>["data"]>[number];

export const Route = createFileRoute("/billing_/plans")({
component: PlansPage,
});
Expand Down Expand Up @@ -44,7 +46,9 @@ function PlansPage() {

const isLoading = customerLoading || plansLoading;

const paidPlans = (plans ?? []).filter((p) => p.id === "hobby" || p.id === "professional");
const paidPlans = (plans ?? ([] as Plan[])).filter(
(p) => p.id === "hobby" || p.id === "professional",
);

return (
<div className="min-h-0 flex-1 overflow-y-auto">
Expand Down
Loading
Loading