diff options
Diffstat (limited to 'packages/console/core/src/user.ts')
| -rw-r--r-- | packages/console/core/src/user.ts | 178 |
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()))) + }) + }) } |
