Skip to content

Commit 5b1fd7e

Browse files
committed
wip: zen
1 parent d18b667 commit 5b1fd7e

File tree

7 files changed

+79
-72
lines changed

7 files changed

+79
-72
lines changed

packages/console/app/src/context/auth.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
7474
userID: user.id,
7575
workspaceID: user.workspaceID,
7676
accountID: user.accountID,
77+
role: user.role,
7778
},
7879
}
7980
}

packages/console/app/src/routes/workspace/[id].tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ export default function () {
4848
<div data-slot="sections">
4949
<NewUserSection />
5050
<KeySection />
51+
<Show when={isBeta()}>
52+
<MemberSection />
53+
</Show>
5154
<Show when={userInfo()?.isAdmin}>
5255
<Show when={isBeta()}>
5356
<SettingsSection />
54-
<MemberSection />
5557
<ModelSection />
5658
<ProviderSection />
5759
</Show>

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@ import { createStore } from "solid-js/store"
77
import { formatDateUTC, formatDateForTable } from "./common"
88
import styles from "./key-section.module.css"
99
import { Actor } from "@opencode-ai/console-core/actor.js"
10-
import { and, Database, eq, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
11-
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
12-
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
13-
import { AccountTable } from "@opencode-ai/console-core/schema/account.sql.js"
14-
import { User } from "@opencode-ai/console-core/user.js"
1510

1611
const removeKey = action(async (form: FormData) => {
1712
"use server"

packages/console/app/src/routes/workspace/member-section.tsx

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import { User } from "@opencode-ai/console-core/user.js"
1010
const listMembers = query(async (workspaceID: string) => {
1111
"use server"
1212
return withActor(async () => {
13-
const actor = Actor.assert("user")
1413
return {
1514
members: await User.list(),
16-
currentUserID: actor.properties.userID,
15+
actorID: Actor.userID(),
16+
actorRole: Actor.userRole(),
1717
}
1818
}, workspaceID)
1919
}, "member.list")
@@ -158,10 +158,11 @@ export function MemberCreateForm() {
158158
)
159159
}
160160

161-
function MemberRow(props: { member: any; workspaceID: string; currentUserID: string | null }) {
161+
function MemberRow(props: { member: any; workspaceID: string; actorID: string; actorRole: string }) {
162162
const [editing, setEditing] = createSignal(false)
163163
const submission = useSubmission(updateMember)
164-
const isCurrentUser = () => props.currentUserID === props.member.id
164+
const isCurrentUser = () => props.actorID === props.member.id
165+
const isAdmin = () => props.actorRole === "admin"
165166

166167
createEffect(() => {
167168
if (!submission.pending && submission.result && !submission.result.error) {
@@ -200,19 +201,19 @@ function MemberRow(props: { member: any; workspaceID: string; currentUserID: str
200201
<td data-slot="member-email">{props.member.accountEmail ?? props.member.email}</td>
201202
<td data-slot="member-role">{props.member.role}</td>
202203
<td data-slot="member-usage">{getUsageDisplay()}</td>
203-
<Show when={!props.member.timeSeen} fallback={<td data-slot="member-joined"></td>}>
204-
<td data-slot="member-joined">invited</td>
205-
</Show>
204+
<td data-slot="member-joined">{props.member.timeSeen ? "" : "invited"}</td>
206205
<td data-slot="member-actions">
207-
<button data-color="ghost" onClick={() => setEditing(true)}>
208-
Edit
209-
</button>
210-
<Show when={!isCurrentUser()}>
211-
<form action={removeMember} method="post">
212-
<input type="hidden" name="id" value={props.member.id} />
213-
<input type="hidden" name="workspaceID" value={props.workspaceID} />
214-
<button data-color="ghost">Delete</button>
215-
</form>
206+
<Show when={isAdmin()}>
207+
<button data-color="ghost" onClick={() => setEditing(true)}>
208+
Edit
209+
</button>
210+
<Show when={!isCurrentUser()}>
211+
<form action={removeMember} method="post">
212+
<input type="hidden" name="id" value={props.member.id} />
213+
<input type="hidden" name="workspaceID" value={props.workspaceID} />
214+
<button data-color="ghost">Delete</button>
215+
</form>
216+
</Show>
216217
</Show>
217218
</td>
218219
</tr>
@@ -293,37 +294,34 @@ export function MemberSection() {
293294
<section class={styles.root}>
294295
<div data-slot="section-title">
295296
<h2>Members</h2>
296-
<p>Manage your members for accessing opencode services.</p>
297297
</div>
298-
<MemberCreateForm />
298+
<Show when={data()?.actorRole === "admin"}>
299+
<MemberCreateForm />
300+
</Show>
299301
<div data-slot="members-table">
300-
<Show
301-
when={data()?.members.length}
302-
fallback={
303-
<div data-component="empty-state">
304-
<p>Invite a member to your workspace</p>
305-
</div>
306-
}
307-
>
308-
<table data-slot="members-table-element">
309-
<thead>
310-
<tr>
311-
<th>Email</th>
312-
<th>Role</th>
313-
<th>Usage</th>
314-
<th></th>
315-
<th></th>
316-
</tr>
317-
</thead>
318-
<tbody>
319-
<For each={data()!.members}>
320-
{(member) => (
321-
<MemberRow member={member} workspaceID={params.id} currentUserID={data()!.currentUserID} />
322-
)}
323-
</For>
324-
</tbody>
325-
</table>
326-
</Show>
302+
<table data-slot="members-table-element">
303+
<thead>
304+
<tr>
305+
<th>Email</th>
306+
<th>Role</th>
307+
<th>Usage</th>
308+
<th></th>
309+
<th></th>
310+
</tr>
311+
</thead>
312+
<tbody>
313+
<For each={data()?.members || []}>
314+
{(member) => (
315+
<MemberRow
316+
member={member}
317+
workspaceID={params.id}
318+
actorID={data()!.actorID}
319+
actorRole={data()!.actorRole}
320+
/>
321+
)}
322+
</For>
323+
</tbody>
324+
</table>
327325
</div>
328326
</section>
329327
)

packages/console/core/src/actor.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Context } from "./context"
2+
import { UserRole } from "./schema/user.sql"
23
import { Log } from "./util/log"
34

45
export namespace Actor {
@@ -21,6 +22,7 @@ export namespace Actor {
2122
userID: string
2223
workspaceID: string
2324
accountID: string
25+
role: (typeof UserRole)[number]
2426
}
2527
}
2628

@@ -80,4 +82,12 @@ export namespace Actor {
8082
}
8183
throw new Error(`actor of type "${actor.type}" is not associated with an account`)
8284
}
85+
86+
export function userID() {
87+
return Actor.assert("user").properties.userID
88+
}
89+
90+
export function userRole() {
91+
return Actor.assert("user").properties.role
92+
}
8393
}

packages/console/core/src/key.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import { User } from "./user"
1010

1111
export namespace Key {
1212
export const list = fn(z.void(), async () => {
13-
const userID = Actor.assert("user").properties.userID
14-
const user = await User.fromID(userID)
1513
const keys = await Database.use((tx) =>
1614
tx
1715
.select({
@@ -30,7 +28,7 @@ export namespace Key {
3028
...[
3129
eq(KeyTable.workspaceID, Actor.workspace()),
3230
isNull(KeyTable.timeDeleted),
33-
...(user.role === "admin" ? [] : [eq(KeyTable.userID, userID)]),
31+
...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]),
3432
],
3533
),
3634
)
@@ -39,7 +37,7 @@ export namespace Key {
3937
// only return value for user's keys
4038
return keys.map((key) => ({
4139
...key,
42-
key: key.userID === userID ? key.key : undefined,
40+
key: key.userID === Actor.userID() ? key.key : undefined,
4341
keyDisplay: `${key.key.slice(0, 7)}...${key.key.slice(-4)}`,
4442
}))
4543
})
@@ -78,14 +76,22 @@ export namespace Key {
7876
)
7977

8078
export const remove = fn(z.object({ id: z.string() }), async (input) => {
81-
const workspace = Actor.workspace()
82-
await Database.transaction((tx) =>
79+
// only admin can remove other user's keys
80+
await Database.use((tx) =>
8381
tx
8482
.update(KeyTable)
8583
.set({
8684
timeDeleted: sql`now()`,
8785
})
88-
.where(and(eq(KeyTable.id, input.id), eq(KeyTable.workspaceID, workspace))),
86+
.where(
87+
and(
88+
...[
89+
eq(KeyTable.id, input.id),
90+
eq(KeyTable.workspaceID, Actor.workspace()),
91+
...(Actor.userRole() === "admin" ? [] : [eq(KeyTable.userID, Actor.userID())]),
92+
],
93+
),
94+
),
8995
)
9096
})
9197
}

packages/console/core/src/user.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z } from "zod"
2-
import { and, eq, getTableColumns, inArray, isNull, or, sql } from "drizzle-orm"
2+
import { and, eq, getTableColumns, isNull, sql } from "drizzle-orm"
33
import { fn } from "./util/fn"
44
import { Database } from "./drizzle"
55
import { UserRole, UserTable } from "./schema/user.sql"
@@ -13,19 +13,14 @@ import { Key } from "./key"
1313
import { KeyTable } from "./schema/key.sql"
1414

1515
export namespace User {
16-
const assertAdmin = async () => {
17-
const actor = Actor.assert("user")
18-
const user = await User.fromID(actor.properties.userID)
19-
if (user?.role !== "admin") {
20-
throw new Error(`Expected admin user, got ${user?.role}`)
21-
}
16+
const assertAdmin = () => {
17+
if (Actor.userRole() === "admin") return
18+
throw new Error(`Expected admin user, got ${Actor.userRole()}`)
2219
}
2320

2421
const assertNotSelf = (id: string) => {
25-
const actor = Actor.assert("user")
26-
if (actor.properties.userID === id) {
27-
throw new Error(`Expected not self actor, got self actor`)
28-
}
22+
if (Actor.userID() !== id) return
23+
throw new Error(`Expected not self actor, got self actor`)
2924
}
3025

3126
export const list = fn(z.void(), () =>
@@ -70,7 +65,7 @@ export namespace User {
7065
role: z.enum(UserRole),
7166
}),
7267
async ({ email, role }) => {
73-
await assertAdmin()
68+
assertAdmin()
7469
const workspaceID = Actor.workspace()
7570

7671
// create user
@@ -181,7 +176,7 @@ export namespace User {
181176
monthlyLimit: z.number().nullable(),
182177
}),
183178
async ({ id, role, monthlyLimit }) => {
184-
await assertAdmin()
179+
assertAdmin()
185180
if (role === "member") assertNotSelf(id)
186181
return await Database.use((tx) =>
187182
tx
@@ -193,7 +188,7 @@ export namespace User {
193188
)
194189

195190
export const remove = fn(z.string(), async (id) => {
196-
await assertAdmin()
191+
assertAdmin()
197192
assertNotSelf(id)
198193

199194
return await Database.use((tx) =>

0 commit comments

Comments
 (0)