mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-21 09:44:21 +01:00
wip: zen
This commit is contained in:
@@ -74,6 +74,7 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
|
|||||||
userID: user.id,
|
userID: user.id,
|
||||||
workspaceID: user.workspaceID,
|
workspaceID: user.workspaceID,
|
||||||
accountID: user.accountID,
|
accountID: user.accountID,
|
||||||
|
role: user.role,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,10 +48,12 @@ export default function () {
|
|||||||
<div data-slot="sections">
|
<div data-slot="sections">
|
||||||
<NewUserSection />
|
<NewUserSection />
|
||||||
<KeySection />
|
<KeySection />
|
||||||
|
<Show when={isBeta()}>
|
||||||
|
<MemberSection />
|
||||||
|
</Show>
|
||||||
<Show when={userInfo()?.isAdmin}>
|
<Show when={userInfo()?.isAdmin}>
|
||||||
<Show when={isBeta()}>
|
<Show when={isBeta()}>
|
||||||
<SettingsSection />
|
<SettingsSection />
|
||||||
<MemberSection />
|
|
||||||
<ModelSection />
|
<ModelSection />
|
||||||
<ProviderSection />
|
<ProviderSection />
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -7,11 +7,6 @@ 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"
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import { User } from "@opencode-ai/console-core/user.js"
|
|||||||
const listMembers = query(async (workspaceID: string) => {
|
const listMembers = query(async (workspaceID: string) => {
|
||||||
"use server"
|
"use server"
|
||||||
return withActor(async () => {
|
return withActor(async () => {
|
||||||
const actor = Actor.assert("user")
|
|
||||||
return {
|
return {
|
||||||
members: await User.list(),
|
members: await User.list(),
|
||||||
currentUserID: actor.properties.userID,
|
actorID: Actor.userID(),
|
||||||
|
actorRole: Actor.userRole(),
|
||||||
}
|
}
|
||||||
}, workspaceID)
|
}, workspaceID)
|
||||||
}, "member.list")
|
}, "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 [editing, setEditing] = createSignal(false)
|
||||||
const submission = useSubmission(updateMember)
|
const submission = useSubmission(updateMember)
|
||||||
const isCurrentUser = () => props.currentUserID === props.member.id
|
const isCurrentUser = () => props.actorID === props.member.id
|
||||||
|
const isAdmin = () => props.actorRole === "admin"
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!submission.pending && submission.result && !submission.result.error) {
|
if (!submission.pending && submission.result && !submission.result.error) {
|
||||||
@@ -200,10 +201,9 @@ 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-email">{props.member.accountEmail ?? props.member.email}</td>
|
||||||
<td data-slot="member-role">{props.member.role}</td>
|
<td data-slot="member-role">{props.member.role}</td>
|
||||||
<td data-slot="member-usage">{getUsageDisplay()}</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">{props.member.timeSeen ? "" : "invited"}</td>
|
||||||
<td data-slot="member-joined">invited</td>
|
|
||||||
</Show>
|
|
||||||
<td data-slot="member-actions">
|
<td data-slot="member-actions">
|
||||||
|
<Show when={isAdmin()}>
|
||||||
<button data-color="ghost" onClick={() => setEditing(true)}>
|
<button data-color="ghost" onClick={() => setEditing(true)}>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
@@ -214,6 +214,7 @@ function MemberRow(props: { member: any; workspaceID: string; currentUserID: str
|
|||||||
<button data-color="ghost">Delete</button>
|
<button data-color="ghost">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
</Show>
|
</Show>
|
||||||
|
</Show>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@@ -293,18 +294,11 @@ export function MemberSection() {
|
|||||||
<section class={styles.root}>
|
<section class={styles.root}>
|
||||||
<div data-slot="section-title">
|
<div data-slot="section-title">
|
||||||
<h2>Members</h2>
|
<h2>Members</h2>
|
||||||
<p>Manage your members for accessing opencode services.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={data()?.actorRole === "admin"}>
|
||||||
<MemberCreateForm />
|
<MemberCreateForm />
|
||||||
|
</Show>
|
||||||
<div data-slot="members-table">
|
<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">
|
<table data-slot="members-table-element">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -316,14 +310,18 @@ export function MemberSection() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<For each={data()!.members}>
|
<For each={data()?.members || []}>
|
||||||
{(member) => (
|
{(member) => (
|
||||||
<MemberRow member={member} workspaceID={params.id} currentUserID={data()!.currentUserID} />
|
<MemberRow
|
||||||
|
member={member}
|
||||||
|
workspaceID={params.id}
|
||||||
|
actorID={data()!.actorID}
|
||||||
|
actorRole={data()!.actorRole}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Context } from "./context"
|
import { Context } from "./context"
|
||||||
|
import { UserRole } from "./schema/user.sql"
|
||||||
import { Log } from "./util/log"
|
import { Log } from "./util/log"
|
||||||
|
|
||||||
export namespace Actor {
|
export namespace Actor {
|
||||||
@@ -21,6 +22,7 @@ export namespace Actor {
|
|||||||
userID: string
|
userID: string
|
||||||
workspaceID: string
|
workspaceID: string
|
||||||
accountID: 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`)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import { User } from "./user"
|
|||||||
|
|
||||||
export namespace Key {
|
export namespace Key {
|
||||||
export const list = fn(z.void(), async () => {
|
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) =>
|
const keys = await Database.use((tx) =>
|
||||||
tx
|
tx
|
||||||
.select({
|
.select({
|
||||||
@@ -30,7 +28,7 @@ export namespace Key {
|
|||||||
...[
|
...[
|
||||||
eq(KeyTable.workspaceID, Actor.workspace()),
|
eq(KeyTable.workspaceID, Actor.workspace()),
|
||||||
isNull(KeyTable.timeDeleted),
|
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
|
// only return value for user's keys
|
||||||
return keys.map((key) => ({
|
return keys.map((key) => ({
|
||||||
...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)}`,
|
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) => {
|
export const remove = fn(z.object({ id: z.string() }), async (input) => {
|
||||||
const workspace = Actor.workspace()
|
// only admin can remove other user's keys
|
||||||
await Database.transaction((tx) =>
|
await Database.use((tx) =>
|
||||||
tx
|
tx
|
||||||
.update(KeyTable)
|
.update(KeyTable)
|
||||||
.set({
|
.set({
|
||||||
timeDeleted: sql`now()`,
|
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())]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { z } from "zod"
|
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 { fn } from "./util/fn"
|
||||||
import { Database } from "./drizzle"
|
import { Database } from "./drizzle"
|
||||||
import { UserRole, UserTable } from "./schema/user.sql"
|
import { UserRole, UserTable } from "./schema/user.sql"
|
||||||
@@ -13,20 +13,15 @@ import { Key } from "./key"
|
|||||||
import { KeyTable } from "./schema/key.sql"
|
import { KeyTable } from "./schema/key.sql"
|
||||||
|
|
||||||
export namespace User {
|
export namespace User {
|
||||||
const assertAdmin = async () => {
|
const assertAdmin = () => {
|
||||||
const actor = Actor.assert("user")
|
if (Actor.userRole() === "admin") return
|
||||||
const user = await User.fromID(actor.properties.userID)
|
throw new Error(`Expected admin user, got ${Actor.userRole()}`)
|
||||||
if (user?.role !== "admin") {
|
|
||||||
throw new Error(`Expected admin user, got ${user?.role}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const assertNotSelf = (id: string) => {
|
const assertNotSelf = (id: string) => {
|
||||||
const actor = Actor.assert("user")
|
if (Actor.userID() !== id) return
|
||||||
if (actor.properties.userID === id) {
|
|
||||||
throw new Error(`Expected not self actor, got self actor`)
|
throw new Error(`Expected not self actor, got self actor`)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export const list = fn(z.void(), () =>
|
export const list = fn(z.void(), () =>
|
||||||
Database.use((tx) =>
|
Database.use((tx) =>
|
||||||
@@ -70,7 +65,7 @@ export namespace User {
|
|||||||
role: z.enum(UserRole),
|
role: z.enum(UserRole),
|
||||||
}),
|
}),
|
||||||
async ({ email, role }) => {
|
async ({ email, role }) => {
|
||||||
await assertAdmin()
|
assertAdmin()
|
||||||
const workspaceID = Actor.workspace()
|
const workspaceID = Actor.workspace()
|
||||||
|
|
||||||
// create user
|
// create user
|
||||||
@@ -181,7 +176,7 @@ export namespace User {
|
|||||||
monthlyLimit: z.number().nullable(),
|
monthlyLimit: z.number().nullable(),
|
||||||
}),
|
}),
|
||||||
async ({ id, role, monthlyLimit }) => {
|
async ({ id, role, monthlyLimit }) => {
|
||||||
await assertAdmin()
|
assertAdmin()
|
||||||
if (role === "member") assertNotSelf(id)
|
if (role === "member") assertNotSelf(id)
|
||||||
return await Database.use((tx) =>
|
return await Database.use((tx) =>
|
||||||
tx
|
tx
|
||||||
@@ -193,7 +188,7 @@ export namespace User {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const remove = fn(z.string(), async (id) => {
|
export const remove = fn(z.string(), async (id) => {
|
||||||
await assertAdmin()
|
assertAdmin()
|
||||||
assertNotSelf(id)
|
assertNotSelf(id)
|
||||||
|
|
||||||
return await Database.use((tx) =>
|
return await Database.use((tx) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user