| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- import { z } from "zod"
- 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"
- 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"
- import { AccountTable } from "./schema/account.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 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({
- ...getTableColumns(UserTable),
- accountEmail: AccountTable.email,
- })
- .from(UserTable)
- .leftJoin(AccountTable, eq(UserTable.accountID, AccountTable.id))
- .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 getAccountEmail = fn(z.string(), (id) =>
- Database.use((tx) =>
- tx
- .select({
- email: AccountTable.email,
- })
- .from(UserTable)
- .leftJoin(AccountTable, eq(UserTable.accountID, AccountTable.id))
- .where(and(eq(UserTable.workspaceID, Actor.workspace()), eq(UserTable.id, id)))
- .then((rows) => rows[0]?.email),
- ),
- )
- 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.transaction(async (tx) => {
- const user = await fromID(id)
- if (!user) throw new Error("User not found")
- await tx
- .update(UserTable)
- .set({
- ...(user.email
- ? {
- oldEmail: user.email,
- email: null,
- }
- : {
- oldAccountID: user.accountID,
- accountID: null,
- }),
- timeDeleted: sql`now()`,
- })
- .where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, Actor.workspace())))
- })
- })
- }
|