Frank 4 月之前
父節點
當前提交
d18b6673e6
共有 2 個文件被更改,包括 60 次插入30 次删除
  1. 27 23
      packages/console/app/src/routes/workspace/key-section.tsx
  2. 33 7
      packages/console/core/src/key.ts

+ 27 - 23
packages/console/app/src/routes/workspace/key-section.tsx

@@ -7,6 +7,11 @@ 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"
@@ -108,11 +113,6 @@ export function KeySection() {
   const params = useParams()
   const keys = createAsync(() => listKeys(params.id))
 
-  function formatKey(key: string) {
-    if (key.length <= 11) return key
-    return `${key.slice(0, 7)}...${key.slice(-4)}`
-  }
-
   return (
     <section class={styles.root}>
       <div data-slot="section-title">
@@ -134,7 +134,8 @@ export function KeySection() {
               <tr>
                 <th>Name</th>
                 <th>Key</th>
-                <th>Created</th>
+                <th>Created By</th>
+                <th>Last Used</th>
                 <th></th>
               </tr>
             </thead>
@@ -147,24 +148,27 @@ export function KeySection() {
                     <tr>
                       <td data-slot="key-name">{key.name}</td>
                       <td data-slot="key-value">
-                        <button
-                          data-color="ghost"
-                          disabled={copied()}
-                          onClick={async () => {
-                            await navigator.clipboard.writeText(key.key)
-                            setCopied(true)
-                            setTimeout(() => setCopied(false), 1000)
-                          }}
-                          title="Copy API key"
-                        >
-                          <span>{formatKey(key.key)}</span>
-                          <Show when={copied()} fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}>
-                            <IconCheck style={{ width: "14px", height: "14px" }} />
-                          </Show>
-                        </button>
+                        <Show when={key.key} fallback={<span>{key.keyDisplay}</span>}>
+                          <button
+                            data-color="ghost"
+                            disabled={copied()}
+                            onClick={async () => {
+                              await navigator.clipboard.writeText(key.key!)
+                              setCopied(true)
+                              setTimeout(() => setCopied(false), 1000)
+                            }}
+                            title="Copy API key"
+                          >
+                            <span>{key.keyDisplay}</span>
+                            <Show when={copied()} fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}>
+                              <IconCheck style={{ width: "14px", height: "14px" }} />
+                            </Show>
+                          </button>
+                        </Show>
                       </td>
-                      <td data-slot="key-date" title={formatDateUTC(key.timeCreated)}>
-                        {formatDateForTable(key.timeCreated)}
+                      <td data-slot="key-user-email">{key.email}</td>
+                      <td data-slot="key-last-used" title={key.timeUsed ? formatDateUTC(key.timeUsed) : undefined}>
+                        {key.timeUsed ? formatDateForTable(key.timeUsed) : "-"}
                       </td>
                       <td data-slot="key-actions">
                         <form action={removeKey} method="post">

+ 33 - 7
packages/console/core/src/key.ts

@@ -4,19 +4,45 @@ import { Actor } from "./actor"
 import { and, Database, eq, isNull, sql } from "./drizzle"
 import { Identifier } from "./identifier"
 import { KeyTable } from "./schema/key.sql"
+import { AccountTable } from "./schema/account.sql"
+import { UserTable } from "./schema/user.sql"
+import { User } from "./user"
 
 export namespace Key {
-  export const list = async () => {
-    const workspace = Actor.workspace()
+  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()
+        .select({
+          id: KeyTable.id,
+          name: KeyTable.name,
+          key: KeyTable.key,
+          timeUsed: KeyTable.timeUsed,
+          userID: KeyTable.userID,
+          email: AccountTable.email,
+        })
         .from(KeyTable)
-        .where(and(eq(KeyTable.workspaceID, workspace), isNull(KeyTable.timeDeleted)))
-        .orderBy(sql`${KeyTable.timeCreated} DESC`),
+        .innerJoin(UserTable, and(eq(KeyTable.userID, UserTable.id), eq(KeyTable.workspaceID, UserTable.workspaceID)))
+        .innerJoin(AccountTable, eq(UserTable.accountID, AccountTable.id))
+        .where(
+          and(
+            ...[
+              eq(KeyTable.workspaceID, Actor.workspace()),
+              isNull(KeyTable.timeDeleted),
+              ...(user.role === "admin" ? [] : [eq(KeyTable.userID, userID)]),
+            ],
+          ),
+        )
+        .orderBy(sql`${KeyTable.name} DESC`),
     )
-    return keys
-  }
+    // only return value for user's keys
+    return keys.map((key) => ({
+      ...key,
+      key: key.userID === userID ? key.key : undefined,
+      keyDisplay: `${key.key.slice(0, 7)}...${key.key.slice(-4)}`,
+    }))
+  })
 
   export const create = fn(
     z.object({