mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-22 20:05:23 +00:00
core: support managing multiple authenticated accounts with individual workspace access
Enable users to authenticate with multiple accounts and switch between them, accessing workspaces from each account separately.
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
ALTER TABLE `account` ADD `id` text;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
CREATE TABLE `__new_account` (
|
||||
`id` text PRIMARY KEY,
|
||||
`email` text NOT NULL,
|
||||
`url` text NOT NULL,
|
||||
`access_token` text NOT NULL,
|
||||
`refresh_token` text NOT NULL,
|
||||
`token_expiry` integer,
|
||||
`active` integer NOT NULL,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_account`(`email`, `url`, `access_token`, `refresh_token`, `token_expiry`, `active`, `time_created`, `time_updated`) SELECT `email`, `url`, `access_token`, `refresh_token`, `token_expiry`, `active`, `time_created`, `time_updated` FROM `account`;--> statement-breakpoint
|
||||
DROP TABLE `account`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_account` RENAME TO `account`;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;
|
||||
1018
packages/opencode/migration/20260228192110_account_id/snapshot.json
Normal file
1018
packages/opencode/migration/20260228192110_account_id/snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,15 @@
|
||||
import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
|
||||
import { Timestamps } from "@/storage/schema.sql"
|
||||
|
||||
export const AccountTable = sqliteTable(
|
||||
"account",
|
||||
{
|
||||
email: text().notNull(),
|
||||
url: text().notNull(),
|
||||
access_token: text().notNull(),
|
||||
refresh_token: text().notNull(),
|
||||
token_expiry: integer(),
|
||||
active: integer({ mode: "boolean" })
|
||||
.notNull()
|
||||
.$default(() => false),
|
||||
...Timestamps,
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({ columns: [table.email, table.url] }),
|
||||
// uniqueIndex("control_account_active_idx").on(table.email).where(eq(table.active, true)),
|
||||
],
|
||||
)
|
||||
export const AccountTable = sqliteTable("account", {
|
||||
id: text().primaryKey(),
|
||||
email: text().notNull(),
|
||||
url: text().notNull(),
|
||||
access_token: text().notNull(),
|
||||
refresh_token: text().notNull(),
|
||||
token_expiry: integer(),
|
||||
active: integer({ mode: "boolean" })
|
||||
.notNull()
|
||||
.$default(() => false),
|
||||
...Timestamps,
|
||||
})
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { eq, and } from "drizzle-orm"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Database } from "@/storage/db"
|
||||
import { AccountTable } from "./account.sql"
|
||||
import z from "zod"
|
||||
|
||||
export namespace Account {
|
||||
export const Account = z.object({
|
||||
id: z.string(),
|
||||
email: z.string(),
|
||||
url: z.string(),
|
||||
})
|
||||
@@ -12,18 +13,40 @@ export namespace Account {
|
||||
|
||||
function fromRow(row: (typeof AccountTable)["$inferSelect"]): Account {
|
||||
return {
|
||||
id: row.id,
|
||||
email: row.email,
|
||||
url: row.url,
|
||||
}
|
||||
}
|
||||
|
||||
export function account(): Account | undefined {
|
||||
export function active(): Account | undefined {
|
||||
const row = Database.use((db) => db.select().from(AccountTable).where(eq(AccountTable.active, true)).get())
|
||||
return row ? fromRow(row) : undefined
|
||||
}
|
||||
|
||||
export async function token(): Promise<string | undefined> {
|
||||
const row = Database.use((db) => db.select().from(AccountTable).where(eq(AccountTable.active, true)).get())
|
||||
export function list(): Account[] {
|
||||
return Database.use((db) => db.select().from(AccountTable).all().map(fromRow))
|
||||
}
|
||||
|
||||
export async function workspaces(accountID: string): Promise<{ id: string; name: string }[]> {
|
||||
const row = Database.use((db) => db.select().from(AccountTable).where(eq(AccountTable.id, accountID)).get())
|
||||
if (!row) return []
|
||||
|
||||
const access = await token(accountID)
|
||||
if (!access) return []
|
||||
|
||||
const res = await fetch(`${row.url}/api/orgs`, {
|
||||
headers: { authorization: `Bearer ${access}` },
|
||||
})
|
||||
|
||||
if (!res.ok) return []
|
||||
|
||||
const json = (await res.json()) as Array<{ id?: string; name?: string }>
|
||||
return json.map((x) => ({ id: x.id ?? "", name: x.name ?? "" }))
|
||||
}
|
||||
|
||||
export async function token(accountID: string): Promise<string | undefined> {
|
||||
const row = Database.use((db) => db.select().from(AccountTable).where(eq(AccountTable.id, accountID)).get())
|
||||
if (!row) return undefined
|
||||
if (row.token_expiry && row.token_expiry > Date.now()) return row.access_token
|
||||
|
||||
@@ -52,7 +75,7 @@ export namespace Account {
|
||||
refresh_token: json.refresh_token ?? row.refresh_token,
|
||||
token_expiry: json.expires_in ? Date.now() + json.expires_in * 1000 : undefined,
|
||||
})
|
||||
.where(and(eq(AccountTable.email, row.email), eq(AccountTable.url, row.url)))
|
||||
.where(eq(AccountTable.id, row.id))
|
||||
.run(),
|
||||
)
|
||||
|
||||
@@ -122,23 +145,20 @@ export namespace Account {
|
||||
access_token?: string
|
||||
refresh_token?: string
|
||||
expires_in?: number
|
||||
email?: string
|
||||
error?: string
|
||||
error_description?: string
|
||||
}
|
||||
|
||||
if (json.access_token) {
|
||||
let email = json.email
|
||||
if (!email) {
|
||||
const me = await fetch(`${input.server}/api/user`, {
|
||||
headers: { authorization: `Bearer ${json.access_token}` },
|
||||
})
|
||||
const user = (await me.json()) as { email?: string }
|
||||
if (!user.email) {
|
||||
return { type: "error", msg: "No email in response" }
|
||||
}
|
||||
email = user.email
|
||||
const me = await fetch(`${input.server}/api/user`, {
|
||||
headers: { authorization: `Bearer ${json.access_token}` },
|
||||
})
|
||||
const user = (await me.json()) as { id?: string; email?: string }
|
||||
if (!user.id || !user.email) {
|
||||
return { type: "error", msg: "No id or email in response" }
|
||||
}
|
||||
const id = user.id
|
||||
const email = user.email
|
||||
|
||||
const access = json.access_token
|
||||
const expiry = Date.now() + json.expires_in! * 1000
|
||||
@@ -148,6 +168,7 @@ export namespace Account {
|
||||
db.update(AccountTable).set({ active: false }).run()
|
||||
db.insert(AccountTable)
|
||||
.values({
|
||||
id,
|
||||
email,
|
||||
url: input.url,
|
||||
access_token: access,
|
||||
@@ -156,7 +177,7 @@ export namespace Account {
|
||||
active: true,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [AccountTable.email, AccountTable.url],
|
||||
target: AccountTable.id,
|
||||
set: {
|
||||
access_token: access,
|
||||
refresh_token: refresh,
|
||||
|
||||
@@ -107,7 +107,8 @@ export namespace Config {
|
||||
}
|
||||
}
|
||||
|
||||
const token = await Account.token()
|
||||
const active = Account.active()
|
||||
const token = active ? await Account.token(active.id) : undefined
|
||||
if (token) {
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user