diff options
| author | Frank <[email protected]> | 2025-10-08 18:59:41 -0400 |
|---|---|---|
| committer | Frank <[email protected]> | 2025-10-08 18:59:41 -0400 |
| commit | 5b1fd7e5397eb72a6f710a3d9c183f2c5dbf3562 (patch) | |
| tree | 72b0f2240fde295fca072534e5076828d9e58aa4 | |
| parent | d18b6673e6f81472bf4486d911f20562c3c7ef91 (diff) | |
| download | opencode-5b1fd7e5397eb72a6f710a3d9c183f2c5dbf3562.tar.gz opencode-5b1fd7e5397eb72a6f710a3d9c183f2c5dbf3562.zip | |
wip: zen
| -rw-r--r-- | packages/console/app/src/context/auth.ts | 1 | ||||
| -rw-r--r-- | packages/console/app/src/routes/workspace/[id].tsx | 4 | ||||
| -rw-r--r-- | packages/console/app/src/routes/workspace/key-section.tsx | 5 | ||||
| -rw-r--r-- | packages/console/app/src/routes/workspace/member-section.tsx | 88 | ||||
| -rw-r--r-- | packages/console/core/src/actor.ts | 10 | ||||
| -rw-r--r-- | packages/console/core/src/key.ts | 20 | ||||
| -rw-r--r-- | packages/console/core/src/user.ts | 23 |
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) => |
