diff --git a/packages/console/app/src/context/auth.ts b/packages/console/app/src/context/auth.ts index 14f27556..c177049c 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 => { 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 a44ddd92..15aeb57a 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 () {
+ + + - diff --git a/packages/console/app/src/routes/workspace/key-section.tsx b/packages/console/app/src/routes/workspace/key-section.tsx index 1c2316db..3b7e399a 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 6774bb48..b13e8e5e 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 {props.member.accountEmail ?? props.member.email} {props.member.role} {getUsageDisplay()} - }> - invited - + {props.member.timeSeen ? "" : "invited"} - - -
- - - -
+ + + +
+ + + +
+
@@ -293,37 +294,34 @@ export function MemberSection() {

Members

-

Manage your members for accessing opencode services.

- + + +
- -

Invite a member to your workspace

-
- } - > - - - - - - - - - - - - - {(member) => ( - - )} - - -
EmailRoleUsage
- + + + + + + + + + + + + + {(member) => ( + + )} + + +
EmailRoleUsage
) diff --git a/packages/console/core/src/actor.ts b/packages/console/core/src/actor.ts index ae11335f..88c5e4b5 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 3a4426d2..e2d5c5ef 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 5e7605e9..38c8e5e3 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) =>