| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- 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 { AWS } from "./aws"
- import { Key } from "./key"
- import { KeyTable } from "./schema/key.sql"
- import { WorkspaceTable } from "./schema/workspace.sql"
- import { AuthTable } from "./schema/auth.sql"
- export namespace User {
- const assertNotSelf = (id: string) => {
- if (Actor.userID() !== id) return
- throw new Error(`Expected not self actor, got self actor`)
- }
- export const list = fn(z.void(), () =>
- Database.use((tx) =>
- tx
- .select({
- ...getTableColumns(UserTable),
- authEmail: AuthTable.subject,
- })
- .from(UserTable)
- .leftJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
- .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 getAuthEmail = fn(z.string(), (id) =>
- Database.use((tx) =>
- tx
- .select({
- email: AuthTable.subject,
- })
- .from(UserTable)
- .leftJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
- .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),
- monthlyLimit: z.number().nullable().optional(),
- }),
- async ({ email, role, monthlyLimit }) => {
- Actor.assertAdmin()
- const workspaceID = Actor.workspace()
- // create user
- const accountID = await Database.use((tx) =>
- tx
- .select({
- accountID: AuthTable.accountID,
- })
- .from(AuthTable)
- .where(and(eq(AuthTable.provider, "email"), eq(AuthTable.subject, email)))
- .then((rows) => rows[0]?.accountID),
- )
- await Database.use((tx) =>
- tx
- .insert(UserTable)
- .values({
- id: Identifier.create("user"),
- name: "",
- ...(accountID
- ? {
- accountID,
- }
- : {
- email,
- }),
- workspaceID,
- role,
- monthlyLimit,
- })
- .onDuplicateKeyUpdate({
- set: {
- role,
- monthlyLimit,
- timeDeleted: null,
- },
- }),
- )
- // create api key
- if (accountID) {
- await Database.use(async (tx) => {
- const user = await tx
- .select()
- .from(UserTable)
- .where(and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.accountID, accountID)))
- .then((rows) => rows[0])
- const key = await tx
- .select()
- .from(KeyTable)
- .where(and(eq(KeyTable.workspaceID, workspaceID), eq(KeyTable.userID, user.id)))
- .then((rows) => rows[0])
- if (key) return
- await Key.create({ userID: user.id, name: "Default API Key" })
- })
- }
- // send email, ignore errors
- try {
- const emailInfo = await Database.use((tx) =>
- tx
- .select({
- inviterEmail: AuthTable.subject,
- workspaceName: WorkspaceTable.name,
- })
- .from(UserTable)
- .innerJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
- .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, workspaceID))
- .where(
- and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.id, Actor.assert("user").properties.userID)),
- )
- .then((rows) => rows[0]),
- )
- const { InviteEmail } = await import("@opencode-ai/console-mail/InviteEmail.jsx")
- await AWS.sendEmail({
- to: email,
- subject: `You've been invited to join the ${emailInfo.workspaceName} workspace on OpenCode`,
- body: render(
- // @ts-ignore
- InviteEmail({
- inviter: emailInfo.inviterEmail,
- assetsUrl: `https://opencode.ai/email`,
- workspaceID: workspaceID,
- workspaceName: emailInfo.workspaceName,
- }),
- ),
- })
- } catch (e) {
- console.error(e)
- }
- },
- )
- export const joinInvitedWorkspaces = fn(z.void(), async () => {
- const account = Actor.assert("account")
- const invitations = await Database.use(async (tx) => {
- const invitations = await tx
- .select({
- id: UserTable.id,
- workspaceID: UserTable.workspaceID,
- })
- .from(UserTable)
- .where(eq(UserTable.email, account.properties.email))
- await tx
- .update(UserTable)
- .set({
- accountID: account.properties.accountID,
- email: null,
- })
- .where(eq(UserTable.email, account.properties.email))
- return invitations
- })
- await Promise.all(
- invitations.map((invite) =>
- Actor.provide(
- "system",
- {
- workspaceID: invite.workspaceID,
- },
- () => Key.create({ userID: invite.id, name: "Default API Key" }),
- ),
- ),
- )
- })
- export const update = fn(
- z.object({
- id: z.string(),
- role: z.enum(UserRole),
- monthlyLimit: z.number().nullable(),
- }),
- async ({ id, role, monthlyLimit }) => {
- Actor.assertAdmin()
- if (role === "member") assertNotSelf(id)
- return await Database.use((tx) =>
- tx
- .update(UserTable)
- .set({ role, monthlyLimit })
- .where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, Actor.workspace()))),
- )
- },
- )
- export const remove = fn(z.string(), async (id) => {
- Actor.assertAdmin()
- assertNotSelf(id)
- return await Database.use((tx) =>
- tx
- .update(UserTable)
- .set({
- timeDeleted: sql`now()`,
- })
- .where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, Actor.workspace()))),
- )
- })
- }
|