diff --git a/packages/console/app/src/routes/api/support/actions/unblock-workspace.ts b/packages/console/app/src/routes/api/support/actions/unblock-workspace.ts index 7a89f99647..9ecede1f38 100644 --- a/packages/console/app/src/routes/api/support/actions/unblock-workspace.ts +++ b/packages/console/app/src/routes/api/support/actions/unblock-workspace.ts @@ -4,7 +4,10 @@ import { safeEqual } from "@opencode-ai/console-core/util/crypto.js" import { Resource } from "@opencode-ai/console-resource" import z from "zod" -const Body = z.object({ workspaceID: z.string().startsWith("wrk_") }) +const Body = z.union([ + z.object({ workspaceID: z.string().startsWith("wrk_") }), + z.object({ email: z.email().transform((email) => email.trim().toLowerCase()) }), +]) export async function POST(event: APIEvent) { if (!safeEqual(event.request.headers.get("authorization") ?? "", `Bearer ${Resource.SUPPORT_API_KEY.value}`)) { @@ -15,7 +18,7 @@ export async function POST(event: APIEvent) { if (!body.success) { return Response.json({ error: "Invalid request", issues: body.error.issues }, { status: 400 }) } - return Workspace.unblock(body.data.workspaceID) - .then(() => Response.json({ success: true, message: "Workspace unblocked" })) + return Workspace.unblock(body.data) + .then((workspaceIDs) => Response.json({ success: true, message: "Workspace unblocked", workspaceIDs })) .catch((error) => Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 })) } diff --git a/packages/console/core/src/workspace.ts b/packages/console/core/src/workspace.ts index 00662bb511..72c0f0887d 100644 --- a/packages/console/core/src/workspace.ts +++ b/packages/console/core/src/workspace.ts @@ -7,8 +7,9 @@ import { UserTable } from "./schema/user.sql" import { BillingTable } from "./schema/billing.sql" import { WorkspaceTable } from "./schema/workspace.sql" import { AccountTable } from "./schema/account.sql" +import { AuthTable } from "./schema/auth.sql" import { Key } from "./key" -import { and, eq, isNull, sql } from "drizzle-orm" +import { and, eq, inArray, isNull, sql } from "drizzle-orm" export namespace Workspace { export const Region = z.enum(["us", "eu", "sg", "cn"]) @@ -104,15 +105,43 @@ export namespace Workspace { ) }) - export const unblock = fn(z.string().startsWith("wrk_"), async (workspaceID) => { - await Database.transaction(async (tx) => { - const workspace = await tx - .select({ id: WorkspaceTable.id }) - .from(WorkspaceTable) - .where(eq(WorkspaceTable.id, workspaceID)) - .then((rows) => rows[0]) - if (!workspace) throw new Error("Workspace not found") - await tx.update(WorkspaceTable).set({ is_blocked: false }).where(eq(WorkspaceTable.id, workspaceID)) - }) - }) + export const unblock = fn( + z.union([ + z.object({ workspaceID: z.string().startsWith("wrk_") }), + z.object({ email: z.email() }), + ]), + async (input) => { + return Database.transaction(async (tx) => { + const workspaces = "workspaceID" in input + ? await tx + .select({ id: WorkspaceTable.id }) + .from(WorkspaceTable) + .where(and(eq(WorkspaceTable.id, input.workspaceID), isNull(WorkspaceTable.timeDeleted))) + : await tx + .selectDistinct({ id: WorkspaceTable.id }) + .from(WorkspaceTable) + .innerJoin( + UserTable, + and(eq(UserTable.workspaceID, WorkspaceTable.id), isNull(UserTable.timeDeleted)), + ) + .innerJoin( + AuthTable, + and( + eq(AuthTable.accountID, UserTable.accountID), + eq(AuthTable.provider, "email"), + eq(AuthTable.subject, input.email.toLowerCase()), + isNull(AuthTable.timeDeleted), + ), + ) + .where(isNull(WorkspaceTable.timeDeleted)) + if (workspaces.length === 0) throw new Error("Workspace not found") + if (!("workspaceID" in input) && workspaces.length > 1) { + throw new Error("Email is associated with multiple workspaces; use workspaceID") + } + const workspaceIDs = workspaces.map((workspace) => workspace.id) + await tx.update(WorkspaceTable).set({ is_blocked: false }).where(inArray(WorkspaceTable.id, workspaceIDs)) + return workspaceIDs + }) + }, + ) }