feat(console): keep migrated teams on the new Console (#46830)
This commit is contained in:
parent
69c172e8a7
commit
50efc055de
6 changed files with 112 additions and 18 deletions
|
|
@ -221,6 +221,15 @@ const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
|
|||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||
properties: { value: auth.url.apply((url) => url!) },
|
||||
})
|
||||
// Preview branches have independent databases; do not send their workspaces to shared dev.
|
||||
const migrationDomain =
|
||||
$app.stage === "production" ? "opencode.ai" : $app.stage === "dev" ? "dev.opencode.ai" : undefined
|
||||
const consoleMigration = new sst.Linkable("ConsoleMigration", {
|
||||
properties: {
|
||||
consoleUrl: migrationDomain ? `https://${migrationDomain}/console` : "",
|
||||
inferenceUrl: migrationDomain ? `https://${migrationDomain}/inference` : "",
|
||||
},
|
||||
})
|
||||
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
|
||||
properties: { value: stripeWebhook.secret },
|
||||
})
|
||||
|
|
@ -255,6 +264,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||
SECRET.UpstashRedisRestUrl,
|
||||
SECRET.UpstashRedisRestToken,
|
||||
AUTH_API_URL,
|
||||
consoleMigration,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
SECRET.SupportApiKey,
|
||||
DISCORD_INCIDENT_WEBHOOK_URL,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { getRequestEvent } from "solid-js/web"
|
||||
import { and, Database, eq, inArray, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||
import { redirect } from "@solidjs/router"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
|
||||
|
|
@ -79,8 +80,15 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
|
|||
if (accounts.length) {
|
||||
const user = await Database.use((tx) =>
|
||||
tx
|
||||
.select()
|
||||
.select({
|
||||
id: UserTable.id,
|
||||
workspaceID: UserTable.workspaceID,
|
||||
accountID: UserTable.accountID,
|
||||
role: UserTable.role,
|
||||
migratedAt: WorkspaceTable.migrated_at,
|
||||
})
|
||||
.from(UserTable)
|
||||
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
|
||||
.where(
|
||||
and(
|
||||
eq(UserTable.workspaceID, workspace),
|
||||
|
|
@ -93,6 +101,15 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
|
|||
.then((x) => x[0]),
|
||||
)
|
||||
if (user) {
|
||||
if (user.migratedAt) {
|
||||
const destination = Resource.ConsoleMigration.consoleUrl
|
||||
if (!destination) throw new Error("New Console URL is not configured")
|
||||
evt.response.headers.set("Cache-Control", "no-store")
|
||||
throw redirect(`${destination}/login`, {
|
||||
status: evt.request.method === "GET" || evt.request.method === "HEAD" ? 302 : 303,
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
})
|
||||
}
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.update(UserTable)
|
||||
|
|
|
|||
55
packages/console/app/src/lib/inference-proxy.ts
Normal file
55
packages/console/app/src/lib/inference-proxy.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||
|
||||
const paths: Record<string, string | undefined> = {
|
||||
"GET /zen/v1/models": "/openai/v1/models",
|
||||
"POST /zen/v1/chat/completions": "/openai/v1/chat/completions",
|
||||
"POST /zen/v1/responses": "/openai/v1/responses",
|
||||
"POST /zen/v1/messages": "/anthropic/v1/messages",
|
||||
}
|
||||
|
||||
export async function proxyInference(request: Request, clientIP?: string): Promise<Response | undefined> {
|
||||
const url = new URL(request.url)
|
||||
const path =
|
||||
paths[`${request.method} ${url.pathname}`] ??
|
||||
(request.method === "POST" &&
|
||||
/^\/zen\/v1\/models\/[^/]+:(?:generateContent|streamGenerateContent)$/.test(url.pathname)
|
||||
? url.pathname.replace("/zen/v1/models/", "/google/v1beta/models/")
|
||||
: undefined)
|
||||
if (!path) return undefined
|
||||
|
||||
const key = path.startsWith("/anthropic/")
|
||||
? request.headers.get("x-api-key")
|
||||
: path.startsWith("/google/")
|
||||
? request.headers.get("x-goog-api-key")
|
||||
: request.headers.get("authorization")?.split(" ")[1]
|
||||
if (!key || key === "public") return undefined
|
||||
|
||||
// Routing only; the destination owns authentication and revocation after cutover.
|
||||
const workspace = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ migratedAt: WorkspaceTable.migrated_at })
|
||||
.from(KeyTable)
|
||||
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
|
||||
.where(eq(KeyTable.key, key))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (!workspace?.migratedAt) return undefined
|
||||
|
||||
const destination = new URL(Resource.ConsoleMigration.inferenceUrl)
|
||||
destination.pathname = `${destination.pathname.replace(/\/$/, "")}${path}`
|
||||
destination.search = url.search
|
||||
destination.hash = ""
|
||||
|
||||
const forwarded = new Request(destination, request)
|
||||
forwarded.headers.set("authorization", `Bearer ${key}`)
|
||||
const ip = request.headers.get("cf-connecting-ip") ?? clientIP
|
||||
if (ip) forwarded.headers.set("x-real-ip", ip)
|
||||
const requestID = request.headers.get("x-opencode-request-id") ?? request.headers.get("x-opencode-request")
|
||||
if (requestID) forwarded.headers.set("x-opencode-request-id", requestID)
|
||||
|
||||
return fetch(forwarded, { redirect: "error" })
|
||||
}
|
||||
|
|
@ -2,9 +2,10 @@ import { createMiddleware } from "@solidjs/start/middleware"
|
|||
import { LOCALE_HEADER, cookie, fromPathname, strip } from "~/lib/language"
|
||||
import { normalizeReferralCode, referralCookie } from "~/lib/referral-invite"
|
||||
import { sanitizeServerActionRequest } from "~/lib/server-action"
|
||||
import { proxyInference } from "~/lib/inference-proxy"
|
||||
|
||||
export default createMiddleware({
|
||||
onRequest(event) {
|
||||
async onRequest(event) {
|
||||
event.request = sanitizeServerActionRequest(event.request)
|
||||
|
||||
const url = new URL(event.request.url)
|
||||
|
|
@ -19,5 +20,12 @@ export default createMiddleware({
|
|||
|
||||
const referralCode = normalizeReferralCode(url.searchParams.get("ref"))
|
||||
if (referralCode) event.response.headers.append("set-cookie", referralCookie(referralCode))
|
||||
|
||||
return proxyInference(event.request, event.clientAddress).catch(() =>
|
||||
Response.json(
|
||||
{ error: { type: "api_error", message: "Inference routing is unavailable. Please retry later." } },
|
||||
{ status: 503, headers: { "Cache-Control": "no-store" } },
|
||||
),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -38,21 +38,25 @@ const setReload = action(async (form: FormData) => {
|
|||
}
|
||||
|
||||
return json(
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
reload: reloadValue,
|
||||
...(reloadAmount !== null ? { reloadAmount } : {}),
|
||||
...(reloadTrigger !== null ? { reloadTrigger } : {}),
|
||||
...(reloadValue
|
||||
? {
|
||||
reloadError: null,
|
||||
timeReloadError: null,
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID)),
|
||||
await withActor(
|
||||
() =>
|
||||
Database.use((tx) =>
|
||||
tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
reload: reloadValue,
|
||||
...(reloadAmount !== null ? { reloadAmount } : {}),
|
||||
...(reloadTrigger !== null ? { reloadTrigger } : {}),
|
||||
...(reloadValue
|
||||
? {
|
||||
reloadError: null,
|
||||
timeReloadError: null,
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID)),
|
||||
),
|
||||
workspaceID,
|
||||
),
|
||||
{ revalidate: queryBillingInfo.key },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ const updateModel = action(async (form: FormData) => {
|
|||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const enabled = (form.get("enabled") as string | null) === "true"
|
||||
return json(
|
||||
withActor(async () => {
|
||||
await withActor(async () => {
|
||||
if (enabled) {
|
||||
await Model.disable({ model })
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in a new issue