mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-19 08:44:22 +01:00
wip: zen
This commit is contained in:
4
bun.lock
4
bun.lock
@@ -48,11 +48,9 @@
|
||||
"name": "@opencode/console-app",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
"@opencode/console-mail": "workspace:*",
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.15.0",
|
||||
"@solidjs/start": "^1.1.0",
|
||||
@@ -66,6 +64,8 @@
|
||||
"version": "0.14.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
"@opencode/console-mail": "workspace:*",
|
||||
"@opencode/console-resource": "workspace:*",
|
||||
"@planetscale/database": "1.19.0",
|
||||
"aws4fetch": "1.0.20",
|
||||
|
||||
@@ -12,10 +12,8 @@
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
"@opencode/console-mail": "workspace:*",
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.15.0",
|
||||
"@solidjs/start": "^1.1.0",
|
||||
|
||||
@@ -79,7 +79,6 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
|
||||
properties: {
|
||||
userID: result.user.id,
|
||||
workspaceID: result.user.workspaceID,
|
||||
role: result.user.role,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,24 +10,14 @@ import { Show } from "solid-js"
|
||||
import { createAsync, query, useParams } from "@solidjs/router"
|
||||
import { Actor } from "@opencode/console-core/actor.js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { and, Database, eq } from "@opencode/console-core/drizzle/index.js"
|
||||
import { UserTable } from "@opencode/console-core/schema/user.sql.js"
|
||||
import { User } from "@opencode/console-core/user.js"
|
||||
|
||||
const getUser = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(async () => {
|
||||
const actor = Actor.use()
|
||||
const isAdmin = await (async () => {
|
||||
if (actor.type !== "user") return false
|
||||
const role = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ role: UserTable.role })
|
||||
.from(UserTable)
|
||||
.where(and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.id, actor.properties.userID))),
|
||||
).then((x) => x[0]?.role)
|
||||
return role === "admin"
|
||||
})()
|
||||
return { isAdmin }
|
||||
const actor = Actor.assert("user")
|
||||
const user = await User.fromID(actor.properties.userID)
|
||||
return { isAdmin: user?.role === "admin" }
|
||||
}, workspaceID)
|
||||
}, "user.get")
|
||||
|
||||
|
||||
@@ -3,46 +3,18 @@ import { createEffect, createSignal, For, Show } from "solid-js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { createStore } from "solid-js/store"
|
||||
import styles from "./member-section.module.css"
|
||||
import { and, Database, eq, isNull, sql } from "@opencode/console-core/drizzle/index.js"
|
||||
import { UserTable, UserRole } from "@opencode/console-core/schema/user.sql.js"
|
||||
import { Identifier } from "@opencode/console-core/identifier.js"
|
||||
import { UserRole } from "@opencode/console-core/schema/user.sql.js"
|
||||
import { Actor } from "@opencode/console-core/actor.js"
|
||||
import { AWS } from "@opencode/console-core/aws.js"
|
||||
|
||||
const assertAdmin = async (workspaceID: string) => {
|
||||
const actor = Actor.use()
|
||||
if (actor.type !== "user") throw new Error(`Expected admin user, got ${actor.type}`)
|
||||
const user = await Database.use((tx) =>
|
||||
tx
|
||||
.select()
|
||||
.from(UserTable)
|
||||
.where(and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.id, actor.properties.userID))),
|
||||
).then((x) => x[0])
|
||||
if (user?.role !== "admin") throw new Error(`Expected admin user, got ${user?.role}`)
|
||||
return actor
|
||||
}
|
||||
|
||||
const assertNotSelf = (id: string) => {
|
||||
const actor = Actor.use()
|
||||
if (actor.type === "user" && actor.properties.userID === id) {
|
||||
throw new Error(`Expected not self actor, got self actor`)
|
||||
}
|
||||
return actor
|
||||
}
|
||||
import { User } from "@opencode/console-core/user.js"
|
||||
|
||||
const listMembers = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(async () => {
|
||||
const actor = await assertAdmin(workspaceID)
|
||||
return Database.use((tx) =>
|
||||
tx
|
||||
.select()
|
||||
.from(UserTable)
|
||||
.where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))),
|
||||
).then((members) => ({
|
||||
members,
|
||||
const actor = Actor.assert("user")
|
||||
return {
|
||||
members: await User.list(),
|
||||
currentUserID: actor.properties.userID,
|
||||
}))
|
||||
}
|
||||
}, workspaceID)
|
||||
}, "member.list")
|
||||
|
||||
@@ -55,43 +27,13 @@ const inviteMember = action(async (form: FormData) => {
|
||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
||||
if (!role) return { error: "Role is required" }
|
||||
return json(
|
||||
await withActor(async () => {
|
||||
await assertAdmin(workspaceID)
|
||||
return Database.use((tx) =>
|
||||
tx
|
||||
.insert(UserTable)
|
||||
.values({
|
||||
id: Identifier.create("user"),
|
||||
name: "",
|
||||
email,
|
||||
workspaceID,
|
||||
role,
|
||||
})
|
||||
await withActor(
|
||||
() =>
|
||||
User.invite({ email, role })
|
||||
.then((data) => ({ error: undefined, data }))
|
||||
.then(async (data) => {
|
||||
const { render } = await import("@jsx-email/render")
|
||||
const { InviteEmail } = await import("@opencode/console-mail/InviteEmail.jsx")
|
||||
await AWS.sendEmail({
|
||||
to: email,
|
||||
subject: `You've been invited to join the ${workspaceID} workspace on OpenCode Zen`,
|
||||
body: render(
|
||||
// @ts-ignore
|
||||
InviteEmail({
|
||||
assetsUrl: `https://opencode.ai/email`,
|
||||
workspace: workspaceID,
|
||||
}),
|
||||
),
|
||||
})
|
||||
return data
|
||||
})
|
||||
.catch((e) => {
|
||||
let error = e.message
|
||||
if (error.match(/Duplicate entry '.*' for key 'user.user_email'/))
|
||||
error = "A user with this email has already been invited."
|
||||
return { error }
|
||||
}),
|
||||
)
|
||||
}, workspaceID),
|
||||
.catch((e) => ({ error: e.message as string })),
|
||||
workspaceID,
|
||||
),
|
||||
{ revalidate: listMembers.key },
|
||||
)
|
||||
}, "member.create")
|
||||
@@ -103,29 +45,13 @@ const removeMember = action(async (form: FormData) => {
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||
return json(
|
||||
await withActor(async () => {
|
||||
await assertAdmin(workspaceID)
|
||||
assertNotSelf(id)
|
||||
return Database.transaction(async (tx) => {
|
||||
const email = await tx
|
||||
.select({ email: UserTable.email })
|
||||
.from(UserTable)
|
||||
.where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, workspaceID)))
|
||||
.execute()
|
||||
.then((rows) => rows[0].email)
|
||||
if (!email) return { error: "User not found" }
|
||||
await tx
|
||||
.update(UserTable)
|
||||
.set({
|
||||
oldEmail: email,
|
||||
email: null,
|
||||
timeDeleted: sql`now()`,
|
||||
})
|
||||
.where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, workspaceID)))
|
||||
})
|
||||
.then(() => ({ error: undefined }))
|
||||
.catch((e) => ({ error: e.message as string }))
|
||||
}, workspaceID),
|
||||
await withActor(
|
||||
() =>
|
||||
User.remove(id)
|
||||
.then((data) => ({ error: undefined, data }))
|
||||
.catch((e) => ({ error: e.message as string })),
|
||||
workspaceID,
|
||||
),
|
||||
{ revalidate: listMembers.key },
|
||||
)
|
||||
}, "member.remove")
|
||||
@@ -139,18 +65,13 @@ const updateMemberRole = action(async (form: FormData) => {
|
||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
||||
if (!role) return { error: "Role is required" }
|
||||
return json(
|
||||
await withActor(async () => {
|
||||
await assertAdmin(workspaceID)
|
||||
if (role === "member") assertNotSelf(id)
|
||||
return Database.use((tx) =>
|
||||
tx
|
||||
.update(UserTable)
|
||||
.set({ role })
|
||||
.where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, workspaceID)))
|
||||
await withActor(
|
||||
() =>
|
||||
User.updateRole({ id, role })
|
||||
.then((data) => ({ error: undefined, data }))
|
||||
.catch((e) => ({ error: e.message as string })),
|
||||
)
|
||||
}, workspaceID),
|
||||
workspaceID,
|
||||
),
|
||||
{ revalidate: listMembers.key },
|
||||
)
|
||||
}, "member.updateRole")
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE `user` ADD `account_id` varchar(30);--> statement-breakpoint
|
||||
ALTER TABLE `user` ADD `old_account_id` varchar(30);--> statement-breakpoint
|
||||
ALTER TABLE `user` ADD CONSTRAINT `user_account_id` UNIQUE(`workspace_id`,`account_id`);
|
||||
724
packages/console/core/migrations/meta/0022_snapshot.json
Normal file
724
packages/console/core/migrations/meta/0022_snapshot.json
Normal file
@@ -0,0 +1,724 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "2296e9e4-bee6-485b-a146-6666ac8dc0d0",
|
||||
"prevId": "14616ba2-c21e-4787-a289-f2a3eb6de04f",
|
||||
"tables": {
|
||||
"account": {
|
||||
"name": "account",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"email": {
|
||||
"name": "email",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"billing": {
|
||||
"name": "billing",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_method_id": {
|
||||
"name": "payment_method_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_method_last4": {
|
||||
"name": "payment_method_last4",
|
||||
"type": "varchar(4)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"balance": {
|
||||
"name": "balance",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monthly_limit": {
|
||||
"name": "monthly_limit",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monthly_usage": {
|
||||
"name": "monthly_usage",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_monthly_usage_updated": {
|
||||
"name": "time_monthly_usage_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reload": {
|
||||
"name": "reload",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reload_error": {
|
||||
"name": "reload_error",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_reload_error": {
|
||||
"name": "time_reload_error",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_reload_locked_till": {
|
||||
"name": "time_reload_locked_till",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"global_customer_id": {
|
||||
"name": "global_customer_id",
|
||||
"columns": [
|
||||
"customer_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"billing_workspace_id_id_pk": {
|
||||
"name": "billing_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"payment": {
|
||||
"name": "payment",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"invoice_id": {
|
||||
"name": "invoice_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_id": {
|
||||
"name": "payment_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"amount": {
|
||||
"name": "amount",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_refunded": {
|
||||
"name": "time_refunded",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"payment_workspace_id_id_pk": {
|
||||
"name": "payment_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"usage": {
|
||||
"name": "usage",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"input_tokens": {
|
||||
"name": "input_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"output_tokens": {
|
||||
"name": "output_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reasoning_tokens": {
|
||||
"name": "reasoning_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_read_tokens": {
|
||||
"name": "cache_read_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_write_5m_tokens": {
|
||||
"name": "cache_write_5m_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_write_1h_tokens": {
|
||||
"name": "cache_write_1h_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"usage_workspace_id_id_pk": {
|
||||
"name": "usage_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"actor": {
|
||||
"name": "actor",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"old_name": {
|
||||
"name": "old_name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_used": {
|
||||
"name": "time_used",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"global_key": {
|
||||
"name": "global_key",
|
||||
"columns": [
|
||||
"key"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"key_workspace_id_id_pk": {
|
||||
"name": "key_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"user": {
|
||||
"name": "user",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"old_account_id": {
|
||||
"name": "old_account_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"old_email": {
|
||||
"name": "old_email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_seen": {
|
||||
"name": "time_seen",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "enum('admin','member')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_account_id": {
|
||||
"name": "user_account_id",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"account_id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"user_email": {
|
||||
"name": "user_email",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"user_workspace_id_id_pk": {
|
||||
"name": "user_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"workspace": {
|
||||
"name": "workspace",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"columns": [
|
||||
"slug"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -155,6 +155,13 @@
|
||||
"when": 1759186023755,
|
||||
"tag": "0021_flawless_clea",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 22,
|
||||
"version": "5",
|
||||
"when": 1759427432588,
|
||||
"tag": "0022_nice_dreadnoughts",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
"@opencode/console-mail": "workspace:*",
|
||||
"@opencode/console-resource": "workspace:*",
|
||||
"@planetscale/database": "1.19.0",
|
||||
"aws4fetch": "1.0.20",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mysqlTable, uniqueIndex, varchar, int, mysqlEnum } from "drizzle-orm/mysql-core"
|
||||
import { timestamps, utc, workspaceColumns } from "../drizzle/types"
|
||||
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
|
||||
import { workspaceIndexes } from "./workspace.sql"
|
||||
|
||||
export const UserRole = ["admin", "member"] as const
|
||||
@@ -9,6 +9,8 @@ export const UserTable = mysqlTable(
|
||||
{
|
||||
...workspaceColumns,
|
||||
...timestamps,
|
||||
accountID: ulid("account_id"),
|
||||
oldAccountID: ulid("old_account_id"),
|
||||
email: varchar("email", { length: 255 }),
|
||||
oldEmail: varchar("old_email", { length: 255 }),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
@@ -16,5 +18,9 @@ export const UserTable = mysqlTable(
|
||||
color: int("color"),
|
||||
role: mysqlEnum("role", UserRole).notNull(),
|
||||
},
|
||||
(table) => [...workspaceIndexes(table), uniqueIndex("user_email").on(table.workspaceID, table.email)],
|
||||
(table) => [
|
||||
...workspaceIndexes(table),
|
||||
uniqueIndex("user_account_id").on(table.workspaceID, table.accountID),
|
||||
uniqueIndex("user_email").on(table.workspaceID, table.email),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,18 +1,180 @@
|
||||
import { z } from "zod"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { and, eq, isNull, sql } from "drizzle-orm"
|
||||
import { fn } from "./util/fn"
|
||||
import { Database } from "./drizzle"
|
||||
import { UserTable } from "./schema/user.sql"
|
||||
import { UserRole, UserTable } from "./schema/user.sql"
|
||||
import { Actor } from "./actor"
|
||||
import { Identifier } from "./identifier"
|
||||
import { render } from "@jsx-email/render"
|
||||
import { InviteEmail } from "@opencode/console-mail/InviteEmail.jsx"
|
||||
import { AWS } from "./aws"
|
||||
import { Account } from "./account"
|
||||
|
||||
export namespace User {
|
||||
export const fromID = fn(z.string(), async (id) =>
|
||||
Database.transaction(async (tx) => {
|
||||
return tx
|
||||
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 assertNotSelf = (id: string) => {
|
||||
const actor = Actor.assert("user")
|
||||
if (actor.properties.userID === id) {
|
||||
throw new Error(`Expected not self actor, got self actor`)
|
||||
}
|
||||
}
|
||||
|
||||
export const list = fn(z.void(), () =>
|
||||
Database.use((tx) =>
|
||||
tx
|
||||
.select()
|
||||
.from(UserTable)
|
||||
.where(eq(UserTable.id, id))
|
||||
.execute()
|
||||
.then((rows) => rows[0])
|
||||
}),
|
||||
.where(and(eq(UserTable.workspaceID, Actor.workspace()), isNull(UserTable.timeDeleted))),
|
||||
),
|
||||
)
|
||||
|
||||
export const fromID = fn(z.string(), (id) =>
|
||||
Database.use((tx) =>
|
||||
tx
|
||||
.select()
|
||||
.from(UserTable)
|
||||
.where(and(eq(UserTable.workspaceID, Actor.workspace()), eq(UserTable.id, id), isNull(UserTable.timeDeleted)))
|
||||
.then((rows) => rows[0]),
|
||||
),
|
||||
)
|
||||
|
||||
export const invite = fn(
|
||||
z.object({
|
||||
email: z.string(),
|
||||
role: z.enum(UserRole),
|
||||
}),
|
||||
async ({ email, role }) => {
|
||||
await assertAdmin()
|
||||
|
||||
const workspaceID = Actor.workspace()
|
||||
await Database.transaction(async (tx) => {
|
||||
const account = await Account.fromEmail(email)
|
||||
const members = await tx.select().from(UserTable).where(eq(UserTable.workspaceID, Actor.workspace()))
|
||||
|
||||
await (async () => {
|
||||
if (account) {
|
||||
// case: account previously invited and removed
|
||||
if (members.some((m) => m.oldAccountID === account.id)) {
|
||||
await tx
|
||||
.update(UserTable)
|
||||
.set({
|
||||
timeDeleted: null,
|
||||
oldAccountID: null,
|
||||
accountID: account.id,
|
||||
})
|
||||
.where(and(eq(UserTable.workspaceID, Actor.workspace()), eq(UserTable.accountID, account.id)))
|
||||
return
|
||||
}
|
||||
// case: account previously not invited
|
||||
await tx
|
||||
.insert(UserTable)
|
||||
.values({
|
||||
id: Identifier.create("user"),
|
||||
name: "",
|
||||
accountID: account.id,
|
||||
workspaceID,
|
||||
role,
|
||||
})
|
||||
.catch((e: any) => {
|
||||
if (e.message.match(/Duplicate entry '.*' for key 'user.user_account_id'/))
|
||||
throw new Error("A user with this email has already been invited.")
|
||||
throw e
|
||||
})
|
||||
return
|
||||
}
|
||||
// case: email previously invited and removed
|
||||
if (members.some((m) => m.oldEmail === email)) {
|
||||
await tx
|
||||
.update(UserTable)
|
||||
.set({
|
||||
timeDeleted: null,
|
||||
oldEmail: null,
|
||||
email,
|
||||
})
|
||||
.where(and(eq(UserTable.workspaceID, Actor.workspace()), eq(UserTable.email, email)))
|
||||
return
|
||||
}
|
||||
// case: email previously not invited
|
||||
await tx
|
||||
.insert(UserTable)
|
||||
.values({
|
||||
id: Identifier.create("user"),
|
||||
name: "",
|
||||
email,
|
||||
workspaceID,
|
||||
role,
|
||||
})
|
||||
.catch((e: any) => {
|
||||
if (e.message.match(/Duplicate entry '.*' for key 'user.user_email'/))
|
||||
throw new Error("A user with this email has already been invited.")
|
||||
throw e
|
||||
})
|
||||
})()
|
||||
})
|
||||
|
||||
// send email, ignore errors
|
||||
try {
|
||||
await AWS.sendEmail({
|
||||
to: email,
|
||||
subject: `You've been invited to join the ${workspaceID} workspace on OpenCode Zen`,
|
||||
body: render(
|
||||
// @ts-ignore
|
||||
InviteEmail({
|
||||
assetsUrl: `https://opencode.ai/email`,
|
||||
workspace: workspaceID,
|
||||
}),
|
||||
),
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const updateRole = fn(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
role: z.enum(UserRole),
|
||||
}),
|
||||
async ({ id, role }) => {
|
||||
await assertAdmin()
|
||||
if (role === "member") assertNotSelf(id)
|
||||
return await Database.use((tx) =>
|
||||
tx
|
||||
.update(UserTable)
|
||||
.set({ role })
|
||||
.where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, Actor.workspace()))),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const remove = fn(z.string(), async (id) => {
|
||||
await assertAdmin()
|
||||
assertNotSelf(id)
|
||||
|
||||
return await Database.use(async (tx) => {
|
||||
const email = await tx
|
||||
.select({ email: UserTable.email })
|
||||
.from(UserTable)
|
||||
.where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, Actor.workspace())))
|
||||
.then((rows) => rows[0]?.email)
|
||||
if (!email) throw new Error("User not found")
|
||||
|
||||
await tx
|
||||
.update(UserTable)
|
||||
.set({
|
||||
oldEmail: email,
|
||||
email: null,
|
||||
timeDeleted: sql`now()`,
|
||||
})
|
||||
.where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, Actor.workspace())))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export namespace Workspace {
|
||||
await tx.insert(UserTable).values({
|
||||
workspaceID,
|
||||
id: Identifier.create("user"),
|
||||
accountID: account.properties.accountID,
|
||||
email: account.properties.email,
|
||||
name: "",
|
||||
role: "admin",
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "react",
|
||||
"types": ["@cloudflare/workers-types", "node"]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user