fix(console): restrict legacy access to Black (#50716)

This commit is contained in:
vprdev
2026-09-22 12:23:33 -07:00
committed by GitHub
parent 6ea95e5e6f
commit fe51b0b19a
6 changed files with 104 additions and 90 deletions
+53 -18
View File
@@ -1,7 +1,8 @@
import { getRequestEvent } from "solid-js/web"
import { and, Database, eq, inArray, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { and, Database, eq, inArray, isNotNull, 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 { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { redirect } from "@solidjs/router"
import { Actor } from "@opencode-ai/console-core/actor.js"
@@ -48,28 +49,14 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
if (!workspace) {
const account = auth.data.account ?? {}
const current = account[auth.data.current ?? ""]
if (current) {
return {
type: "account",
properties: {
email: current.email,
accountID: current.id,
},
}
}
if (current) return requireBlackAccount(current)
if (Object.keys(account).length > 0) {
const current = Object.values(account)[0]
await auth.update((val) => ({
...val,
current: current.id,
}))
return {
type: "account",
properties: {
email: current.email,
accountID: current.id,
},
}
return requireBlackAccount(current)
}
return {
type: "public",
@@ -78,6 +65,22 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
}
const accounts = Object.keys(auth.data.account ?? {})
if (accounts.length) {
const blackAccounts = await Database.use((tx) =>
tx
.selectDistinct({ accountID: UserTable.accountID })
.from(UserTable)
.innerJoin(BillingTable, eq(BillingTable.workspaceID, UserTable.workspaceID))
.where(
and(
inArray(UserTable.accountID, accounts),
isNull(UserTable.timeDeleted),
isNotNull(BillingTable.subscriptionID),
),
)
.then((rows) => rows.map((row) => row.accountID).filter((accountID): accountID is string => !!accountID)),
)
if (!blackAccounts.length) throw redirectToNewConsole()
const user = await Database.use((tx) =>
tx
.select({
@@ -93,7 +96,7 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
and(
eq(UserTable.workspaceID, workspace),
isNull(UserTable.timeDeleted),
inArray(UserTable.accountID, accounts),
inArray(UserTable.accountID, blackAccounts),
),
)
.limit(1)
@@ -131,3 +134,35 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
})()
return evt.locals.actor
}
async function requireBlackAccount(account: { id: string; email: string }): Promise<Actor.Info> {
const black = await Database.use((tx) =>
tx
.select({ id: UserTable.id })
.from(UserTable)
.innerJoin(BillingTable, eq(BillingTable.workspaceID, UserTable.workspaceID))
.where(
and(
eq(UserTable.accountID, account.id),
isNull(UserTable.timeDeleted),
isNotNull(BillingTable.subscriptionID),
),
)
.limit(1)
.then((rows) => rows[0]),
)
if (!black) throw redirectToNewConsole()
return {
type: "account",
properties: {
email: account.email,
accountID: account.id,
},
}
}
function redirectToNewConsole() {
const destination = Resource.ConsoleMigration.consoleUrl
if (!destination) throw new Error("New Console URL is not configured")
return redirect(`${destination}/login`, { status: 302, headers: { "Cache-Control": "no-store" } })
}
@@ -14,7 +14,8 @@ export async function GET(input: APIEvent) {
return redirect(`${destination}/login`, { headers: { "Cache-Control": "no-store" } })
}
return redirect(route(locale, `/workspace/${workspaceID}`))
} catch {
} catch (error) {
if (error instanceof Response) throw error
return redirect("/auth/authorize")
}
}
+1 -1
View File
@@ -59,7 +59,7 @@ const models = [
export default function Home() {
const workspaceID = createAsync(() => checkLoggedIn())
const subscribeUrl = createMemo(() => (workspaceID() ? `/workspace/${workspaceID()}/go` : "/auth"))
const subscribeUrl = createMemo(() => "/console/go")
const i18n = useI18n()
const language = useLanguage()
return (
@@ -1,4 +1,4 @@
import { createAsync, useParams } from "@solidjs/router"
import { createAsync, redirect, useParams } from "@solidjs/router"
import { Show } from "solid-js"
import { IconGo } from "~/component/icon"
import { GoReferralSection, queryGoReferral } from "~/component/go-referral"
@@ -7,6 +7,7 @@ import { useLanguage } from "~/context/language"
import { LiteSection, queryLiteSubscription } from "./lite-section"
export default function () {
throw redirect("/console/go")
const params = useParams()
const i18n = useI18n()
const language = useLanguage()
+1
View File
@@ -304,6 +304,7 @@ export namespace Billing {
}),
async (input) => {
const user = Actor.assert("user")
throw new Error("Go subscriptions have moved to the new Console")
const { successUrl, cancelUrl, method } = input
const email = (await User.getAuthEmail(user.properties.userID))!
+45 -69
View File
@@ -7,15 +7,14 @@ import { THEME_OPENAUTH } from "@openauthjs/openauth/ui/theme"
import { GithubProvider } from "@openauthjs/openauth/provider/github"
import { GoogleOidcProvider } from "@openauthjs/openauth/provider/google"
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
import { Account } from "@opencode-ai/console-core/account.js"
import { Workspace } from "@opencode-ai/console-core/workspace.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { Resource } from "@opencode-ai/console-resource"
import { User } from "@opencode-ai/console-core/user.js"
import { and, Database, eq, isNull, or } from "@opencode-ai/console-core/drizzle/index.js"
import { and, Database, eq, isNotNull, isNull, or } from "@opencode-ai/console-core/drizzle/index.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js"
import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { Identifier } from "@opencode-ai/console-core/identifier.js"
import { isAllowedAuthorizationRedirect } from "./auth-redirect.js"
@@ -115,7 +114,7 @@ export default {
}),
subjects,
allow: ({ clientID, redirectURI }) => Promise.resolve(isAllowedAuthorizationRedirect(clientID, redirectURI)),
async success(ctx, response) {
async success(ctx, response, request) {
console.log(response)
let subject: string | undefined
@@ -155,85 +154,62 @@ export default {
throw new Error("Invalid email")
}
// Get account
let newAccount = false
const accountID = await (async () => {
const matches = await Database.use(async (tx) =>
tx
.select({
provider: AuthTable.provider,
accountID: AuthTable.accountID,
})
.from(AuthTable)
.where(
or(
and(eq(AuthTable.provider, response.provider), eq(AuthTable.subject, subject)),
and(eq(AuthTable.provider, "email"), eq(AuthTable.subject, email)),
),
const matches = await Database.use((tx) =>
tx
.select({ provider: AuthTable.provider, accountID: AuthTable.accountID })
.from(AuthTable)
.where(
or(
and(eq(AuthTable.provider, response.provider), eq(AuthTable.subject, subject)),
and(eq(AuthTable.provider, "email"), eq(AuthTable.subject, email)),
),
)
const idByProvider = matches.find((x) => x.provider === response.provider)?.accountID
const idByEmail = matches.find((x) => x.provider === "email")?.accountID
if (idByProvider && idByEmail) return idByProvider
),
)
const accountID =
matches.find((match) => match.provider === response.provider)?.accountID ??
matches.find((match) => match.provider === "email")?.accountID
if (!accountID) return redirectToNewConsole(request)
// create account if not found
let accountID = idByProvider ?? idByEmail
if (!accountID) {
console.log("creating account for", email)
accountID = await Account.create({})
newAccount = true
}
await Database.use(async (tx) =>
tx
.insert(AuthTable)
.values([
{
id: Identifier.create("auth"),
accountID,
provider: response.provider,
subject,
},
{
id: Identifier.create("auth"),
accountID,
provider: "email",
subject: email,
},
])
.onDuplicateKeyUpdate({
set: {
timeDeleted: null,
},
}),
)
return accountID
})()
// Get workspace
await Actor.provide("account", { accountID, email }, async () => {
const black = await Actor.provide("account", { accountID, email }, async () => {
await User.joinInvitedWorkspaces()
const workspaces = await Database.use((tx) =>
return Database.use((tx) =>
tx
.select({ id: WorkspaceTable.id })
.from(WorkspaceTable)
.innerJoin(UserTable, eq(UserTable.workspaceID, WorkspaceTable.id))
.select({ id: UserTable.id })
.from(UserTable)
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
.innerJoin(BillingTable, eq(BillingTable.workspaceID, UserTable.workspaceID))
.where(
and(
eq(UserTable.accountID, accountID),
isNull(UserTable.timeDeleted),
isNull(WorkspaceTable.timeDeleted),
isNotNull(BillingTable.subscriptionID),
),
),
)
.limit(1)
.then((rows) => rows[0]),
)
if (workspaces.length === 0) {
await Workspace.create({ name: "Default" })
}
})
return ctx.subject("account", accountID, { accountID, email, newAccount })
if (!black) return redirectToNewConsole(request)
await Database.use((tx) =>
tx
.insert(AuthTable)
.values([
{ id: Identifier.create("auth"), accountID, provider: response.provider, subject },
{ id: Identifier.create("auth"), accountID, provider: "email", subject: email },
])
.onDuplicateKeyUpdate({ set: { timeDeleted: null } }),
)
return ctx.subject("account", accountID, { accountID, email, newAccount: false })
},
}).fetch(request, env, ctx)
return result
},
}
function redirectToNewConsole(request: Request) {
const destination = new URL("/console", request.url)
destination.hostname = destination.hostname.replace(/^auth\./, "")
return Response.redirect(destination.toString(), 302)
}