summaryrefslogtreecommitdiffhomepage
path: root/packages/console/core/src/user.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/console/core/src/user.ts')
-rw-r--r--packages/console/core/src/user.ts178
1 files changed, 170 insertions, 8 deletions
diff --git a/packages/console/core/src/user.ts b/packages/console/core/src/user.ts
index 7914926ff..ecf592297 100644
--- a/packages/console/core/src/user.ts
+++ b/packages/console/core/src/user.ts
@@ -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())))
+ })
+ })
}