Frank 4 месяцев назад
Родитель
Сommit
5b1fd7e539

+ 1 - 0
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,
           },
         }
       }

+ 3 - 1
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>

+ 0 - 5
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"

+ 43 - 45
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>
   )

+ 10 - 0
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
+  }
 }

+ 13 - 7
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())]),
+            ],
+          ),
+        ),
     )
   })
 }

+ 9 - 14
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) =>