This commit is contained in:
Frank
2025-10-08 17:03:42 -04:00
parent c93c0d402d
commit d18b6673e6
2 changed files with 60 additions and 30 deletions

View File

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

View File

@@ -4,19 +4,45 @@ import { Actor } from "./actor"
import { and, Database, eq, isNull, sql } from "./drizzle" import { and, Database, eq, isNull, sql } from "./drizzle"
import { Identifier } from "./identifier" import { Identifier } from "./identifier"
import { KeyTable } from "./schema/key.sql" 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 namespace Key {
export const list = async () => { export const list = fn(z.void(), async () => {
const workspace = Actor.workspace() const userID = Actor.assert("user").properties.userID
const user = await User.fromID(userID)
const keys = await Database.use((tx) => const keys = await Database.use((tx) =>
tx tx
.select() .select({
id: KeyTable.id,
name: KeyTable.name,
key: KeyTable.key,
timeUsed: KeyTable.timeUsed,
userID: KeyTable.userID,
email: AccountTable.email,
})
.from(KeyTable) .from(KeyTable)
.where(and(eq(KeyTable.workspaceID, workspace), isNull(KeyTable.timeDeleted))) .innerJoin(UserTable, and(eq(KeyTable.userID, UserTable.id), eq(KeyTable.workspaceID, UserTable.workspaceID)))
.orderBy(sql`${KeyTable.timeCreated} DESC`), .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( export const create = fn(
z.object({ z.object({