summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-10-08 18:59:41 -0400
committerFrank <[email protected]>2025-10-08 18:59:41 -0400
commit5b1fd7e5397eb72a6f710a3d9c183f2c5dbf3562 (patch)
tree72b0f2240fde295fca072534e5076828d9e58aa4
parentd18b6673e6f81472bf4486d911f20562c3c7ef91 (diff)
downloadopencode-5b1fd7e5397eb72a6f710a3d9c183f2c5dbf3562.tar.gz
opencode-5b1fd7e5397eb72a6f710a3d9c183f2c5dbf3562.zip
wip: zen
-rw-r--r--packages/console/app/src/context/auth.ts1
-rw-r--r--packages/console/app/src/routes/workspace/[id].tsx4
-rw-r--r--packages/console/app/src/routes/workspace/key-section.tsx5
-rw-r--r--packages/console/app/src/routes/workspace/member-section.tsx88
-rw-r--r--packages/console/core/src/actor.ts10
-rw-r--r--packages/console/core/src/key.ts20
-rw-r--r--packages/console/core/src/user.ts23
7 files changed, 79 insertions, 72 deletions
diff --git a/packages/console/app/src/context/auth.ts b/packages/console/app/src/context/auth.ts
index 14f275565..c177049c3 100644
--- a/packages/console/app/src/context/auth.ts
+++ b/packages/console/app/src/context/auth.ts
@@ -74,6 +74,7 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
userID: user.id,
workspaceID: user.workspaceID,
accountID: user.accountID,
+ role: user.role,
},
}
}
diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx
index a44ddd927..15aeb57a0 100644
--- a/packages/console/app/src/routes/workspace/[id].tsx
+++ b/packages/console/app/src/routes/workspace/[id].tsx
@@ -48,10 +48,12 @@ export default function () {
<div data-slot="sections">
<NewUserSection />
<KeySection />
+ <Show when={isBeta()}>
+ <MemberSection />
+ </Show>
<Show when={userInfo()?.isAdmin}>
<Show when={isBeta()}>
<SettingsSection />
- <MemberSection />
<ModelSection />
<ProviderSection />
</Show>
diff --git a/packages/console/app/src/routes/workspace/key-section.tsx b/packages/console/app/src/routes/workspace/key-section.tsx
index 1c2316db7..3b7e399aa 100644
--- a/packages/console/app/src/routes/workspace/key-section.tsx
+++ b/packages/console/app/src/routes/workspace/key-section.tsx
@@ -7,11 +7,6 @@ import { createStore } from "solid-js/store"
import { formatDateUTC, formatDateForTable } from "./common"
import styles from "./key-section.module.css"
import { Actor } from "@opencode-ai/console-core/actor.js"
-import { and, Database, eq, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
-import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
-import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
-import { AccountTable } from "@opencode-ai/console-core/schema/account.sql.js"
-import { User } from "@opencode-ai/console-core/user.js"
const removeKey = action(async (form: FormData) => {
"use server"
diff --git a/packages/console/app/src/routes/workspace/member-section.tsx b/packages/console/app/src/routes/workspace/member-section.tsx
index 6774bb48e..b13e8e5ed 100644
--- a/packages/console/app/src/routes/workspace/member-section.tsx
+++ b/packages/console/app/src/routes/workspace/member-section.tsx
@@ -10,10 +10,10 @@ import { User } from "@opencode-ai/console-core/user.js"
const listMembers = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
- const actor = Actor.assert("user")
return {
members: await User.list(),
- currentUserID: actor.properties.userID,
+ actorID: Actor.userID(),
+ actorRole: Actor.userRole(),
}
}, workspaceID)
}, "member.list")
@@ -158,10 +158,11 @@ export function MemberCreateForm() {
)
}
-function MemberRow(props: { member: any; workspaceID: string; currentUserID: string | null }) {
+function MemberRow(props: { member: any; workspaceID: string; actorID: string; actorRole: string }) {
const [editing, setEditing] = createSignal(false)
const submission = useSubmission(updateMember)
- const isCurrentUser = () => props.currentUserID === props.member.id
+ const isCurrentUser = () => props.actorID === props.member.id
+ const isAdmin = () => props.actorRole === "admin"
createEffect(() => {
if (!submission.pending && submission.result && !submission.result.error) {
@@ -200,19 +201,19 @@ function MemberRow(props: { member: any; workspaceID: string; currentUserID: str
<td data-slot="member-email">{props.member.accountEmail ?? props.member.email}</td>
<td data-slot="member-role">{props.member.role}</td>
<td data-slot="member-usage">{getUsageDisplay()}</td>
- <Show when={!props.member.timeSeen} fallback={<td data-slot="member-joined"></td>}>
- <td data-slot="member-joined">invited</td>
- </Show>
+ <td data-slot="member-joined">{props.member.timeSeen ? "" : "invited"}</td>
<td data-slot="member-actions">
- <button data-color="ghost" onClick={() => setEditing(true)}>
- Edit
- </button>
- <Show when={!isCurrentUser()}>
- <form action={removeMember} method="post">
- <input type="hidden" name="id" value={props.member.id} />
- <input type="hidden" name="workspaceID" value={props.workspaceID} />
- <button data-color="ghost">Delete</button>
- </form>
+ <Show when={isAdmin()}>
+ <button data-color="ghost" onClick={() => setEditing(true)}>
+ Edit
+ </button>
+ <Show when={!isCurrentUser()}>
+ <form action={removeMember} method="post">
+ <input type="hidden" name="id" value={props.member.id} />
+ <input type="hidden" name="workspaceID" value={props.workspaceID} />
+ <button data-color="ghost">Delete</button>
+ </form>
+ </Show>
</Show>
</td>
</tr>
@@ -293,37 +294,34 @@ export function MemberSection() {
<section class={styles.root}>
<div data-slot="section-title">
<h2>Members</h2>
- <p>Manage your members for accessing opencode services.</p>
</div>
- <MemberCreateForm />
+ <Show when={data()?.actorRole === "admin"}>
+ <MemberCreateForm />
+ </Show>
<div data-slot="members-table">
- <Show
- when={data()?.members.length}
- fallback={
- <div data-component="empty-state">
- <p>Invite a member to your workspace</p>
- </div>
- }
- >
- <table data-slot="members-table-element">
- <thead>
- <tr>
- <th>Email</th>
- <th>Role</th>
- <th>Usage</th>
- <th></th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <For each={data()!.members}>
- {(member) => (
- <MemberRow member={member} workspaceID={params.id} currentUserID={data()!.currentUserID} />
- )}
- </For>
- </tbody>
- </table>
- </Show>
+ <table data-slot="members-table-element">
+ <thead>
+ <tr>
+ <th>Email</th>
+ <th>Role</th>
+ <th>Usage</th>
+ <th></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <For each={data()?.members || []}>
+ {(member) => (
+ <MemberRow
+ member={member}
+ workspaceID={params.id}
+ actorID={data()!.actorID}
+ actorRole={data()!.actorRole}
+ />
+ )}
+ </For>
+ </tbody>
+ </table>
</div>
</section>
)
diff --git a/packages/console/core/src/actor.ts b/packages/console/core/src/actor.ts
index ae11335f8..88c5e4b51 100644
--- a/packages/console/core/src/actor.ts
+++ b/packages/console/core/src/actor.ts
@@ -1,4 +1,5 @@
import { Context } from "./context"
+import { UserRole } from "./schema/user.sql"
import { Log } from "./util/log"
export namespace Actor {
@@ -21,6 +22,7 @@ export namespace Actor {
userID: string
workspaceID: string
accountID: string
+ role: (typeof UserRole)[number]
}
}
@@ -80,4 +82,12 @@ export namespace Actor {
}
throw new Error(`actor of type "${actor.type}" is not associated with an account`)
}
+
+ export function userID() {
+ return Actor.assert("user").properties.userID
+ }
+
+ export function userRole() {
+ return Actor.assert("user").properties.role
+ }
}
diff --git a/packages/console/core/src/key.ts b/packages/console/core/src/key.ts
index 3a4426d28..e2d5c5eff 100644
--- a/packages/console/core/src/key.ts
+++ b/packages/console/core/src/key.ts
@@ -10,8 +10,6 @@ import { User } from "./user"
export namespace Key {
export const list = fn(z.void(), async () => {
- const userID = Actor.assert("user").properties.userID
- const user = await User.fromID(userID)
const keys = await Database.use((tx) =>
tx
.select({
@@ -30,7 +28,7 @@ export namespace Key {
...[
eq(KeyTable.workspaceID, Actor.workspace()),
isNull(KeyTable.timeDeleted),
- ...(user.role === "admin" ? [] : [eq(KeyTable.userID, userID)]),
+ ...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]),
],
),
)
@@ -39,7 +37,7 @@ export namespace Key {
// only return value for user's keys
return keys.map((key) => ({
...key,
- key: key.userID === userID ? key.key : undefined,
+ key: key.userID === Actor.userID() ? key.key : undefined,
keyDisplay: `${key.key.slice(0, 7)}...${key.key.slice(-4)}`,
}))
})
@@ -78,14 +76,22 @@ export namespace Key {
)
export const remove = fn(z.object({ id: z.string() }), async (input) => {
- const workspace = Actor.workspace()
- await Database.transaction((tx) =>
+ // only admin can remove other user's keys
+ await Database.use((tx) =>
tx
.update(KeyTable)
.set({
timeDeleted: sql`now()`,
})
- .where(and(eq(KeyTable.id, input.id), eq(KeyTable.workspaceID, workspace))),
+ .where(
+ and(
+ ...[
+ eq(KeyTable.id, input.id),
+ eq(KeyTable.workspaceID, Actor.workspace()),
+ ...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]),
+ ],
+ ),
+ ),
)
})
}
diff --git a/packages/console/core/src/user.ts b/packages/console/core/src/user.ts
index 5e7605e94..38c8e5e3a 100644
--- a/packages/console/core/src/user.ts
+++ b/packages/console/core/src/user.ts
@@ -1,5 +1,5 @@
import { z } from "zod"
-import { and, eq, getTableColumns, inArray, isNull, or, sql } from "drizzle-orm"
+import { and, eq, getTableColumns, isNull, sql } from "drizzle-orm"
import { fn } from "./util/fn"
import { Database } from "./drizzle"
import { UserRole, UserTable } from "./schema/user.sql"
@@ -13,19 +13,14 @@ import { Key } from "./key"
import { KeyTable } from "./schema/key.sql"
export namespace User {
- const assertAdmin = async () => {
- const actor = Actor.assert("user")
- const user = await User.fromID(actor.properties.userID)
- if (user?.role !== "admin") {
- throw new Error(`Expected admin user, got ${user?.role}`)
- }
+ const assertAdmin = () => {
+ if (Actor.userRole() === "admin") return
+ throw new Error(`Expected admin user, got ${Actor.userRole()}`)
}
const assertNotSelf = (id: string) => {
- const actor = Actor.assert("user")
- if (actor.properties.userID === id) {
- throw new Error(`Expected not self actor, got self actor`)
- }
+ if (Actor.userID() !== id) return
+ throw new Error(`Expected not self actor, got self actor`)
}
export const list = fn(z.void(), () =>
@@ -70,7 +65,7 @@ export namespace User {
role: z.enum(UserRole),
}),
async ({ email, role }) => {
- await assertAdmin()
+ assertAdmin()
const workspaceID = Actor.workspace()
// create user
@@ -181,7 +176,7 @@ export namespace User {
monthlyLimit: z.number().nullable(),
}),
async ({ id, role, monthlyLimit }) => {
- await assertAdmin()
+ assertAdmin()
if (role === "member") assertNotSelf(id)
return await Database.use((tx) =>
tx
@@ -193,7 +188,7 @@ export namespace User {
)
export const remove = fn(z.string(), async (id) => {
- await assertAdmin()
+ assertAdmin()
assertNotSelf(id)
return await Database.use((tx) =>