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
67 changes: 49 additions & 18 deletions packages/console/app/src/context/auth.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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",
Expand All @@ -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({
Expand All @@ -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)
Expand Down Expand Up @@ -131,3 +134,31 @@ 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" } })
}
3 changes: 2 additions & 1 deletion packages/console/app/src/routes/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
2 changes: 1 addition & 1 deletion packages/console/app/src/routes/go/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
3 changes: 2 additions & 1 deletion packages/console/app/src/routes/workspace/[id]/go/index.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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()
Expand Down
1 change: 1 addition & 0 deletions packages/console/core/src/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))!
Expand Down
114 changes: 45 additions & 69 deletions packages/console/function/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Loading