user.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import { z } from "zod"
  2. import { and, eq, getTableColumns, isNull, sql } from "drizzle-orm"
  3. import { fn } from "./util/fn"
  4. import { Database } from "./drizzle"
  5. import { UserRole, UserTable } from "./schema/user.sql"
  6. import { Actor } from "./actor"
  7. import { Identifier } from "./identifier"
  8. import { render } from "@jsx-email/render"
  9. import { AWS } from "./aws"
  10. import { Account } from "./account"
  11. import { AccountTable } from "./schema/account.sql"
  12. import { Key } from "./key"
  13. import { KeyTable } from "./schema/key.sql"
  14. import { WorkspaceTable } from "./schema/workspace.sql"
  15. export namespace User {
  16. const assertNotSelf = (id: string) => {
  17. if (Actor.userID() !== id) return
  18. throw new Error(`Expected not self actor, got self actor`)
  19. }
  20. export const list = fn(z.void(), () =>
  21. Database.use((tx) =>
  22. tx
  23. .select({
  24. ...getTableColumns(UserTable),
  25. accountEmail: AccountTable.email,
  26. })
  27. .from(UserTable)
  28. .leftJoin(AccountTable, eq(UserTable.accountID, AccountTable.id))
  29. .where(and(eq(UserTable.workspaceID, Actor.workspace()), isNull(UserTable.timeDeleted))),
  30. ),
  31. )
  32. export const fromID = fn(z.string(), (id) =>
  33. Database.use((tx) =>
  34. tx
  35. .select()
  36. .from(UserTable)
  37. .where(and(eq(UserTable.workspaceID, Actor.workspace()), eq(UserTable.id, id), isNull(UserTable.timeDeleted)))
  38. .then((rows) => rows[0]),
  39. ),
  40. )
  41. export const getAccountEmail = fn(z.string(), (id) =>
  42. Database.use((tx) =>
  43. tx
  44. .select({
  45. email: AccountTable.email,
  46. })
  47. .from(UserTable)
  48. .leftJoin(AccountTable, eq(UserTable.accountID, AccountTable.id))
  49. .where(and(eq(UserTable.workspaceID, Actor.workspace()), eq(UserTable.id, id)))
  50. .then((rows) => rows[0]?.email),
  51. ),
  52. )
  53. export const invite = fn(
  54. z.object({
  55. email: z.string(),
  56. role: z.enum(UserRole),
  57. monthlyLimit: z.number().nullable().optional(),
  58. }),
  59. async ({ email, role, monthlyLimit }) => {
  60. Actor.assertAdmin()
  61. const workspaceID = Actor.workspace()
  62. // create user
  63. const account = await Account.fromEmail(email)
  64. await Database.use((tx) =>
  65. tx
  66. .insert(UserTable)
  67. .values({
  68. id: Identifier.create("user"),
  69. name: "",
  70. ...(account
  71. ? {
  72. accountID: account.id,
  73. }
  74. : {
  75. email,
  76. }),
  77. workspaceID,
  78. role,
  79. monthlyLimit,
  80. })
  81. .onDuplicateKeyUpdate({
  82. set: {
  83. role,
  84. monthlyLimit,
  85. timeDeleted: null,
  86. },
  87. }),
  88. )
  89. // create api key
  90. if (account) {
  91. await Database.use(async (tx) => {
  92. const user = await tx
  93. .select()
  94. .from(UserTable)
  95. .where(and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.accountID, account.id)))
  96. .then((rows) => rows[0])
  97. const key = await tx
  98. .select()
  99. .from(KeyTable)
  100. .where(and(eq(KeyTable.workspaceID, workspaceID), eq(KeyTable.userID, user.id)))
  101. .then((rows) => rows[0])
  102. if (key) return
  103. await Key.create({ userID: user.id, name: "Default API Key" })
  104. })
  105. }
  106. // send email, ignore errors
  107. try {
  108. const emailInfo = await Database.use((tx) =>
  109. tx
  110. .select({
  111. email: AccountTable.email,
  112. workspaceName: WorkspaceTable.name,
  113. })
  114. .from(UserTable)
  115. .innerJoin(AccountTable, eq(UserTable.accountID, AccountTable.id))
  116. .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, workspaceID))
  117. .where(
  118. and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.id, Actor.assert("user").properties.userID)),
  119. )
  120. .then((rows) => rows[0]),
  121. )
  122. const { InviteEmail } = await import("@opencode-ai/console-mail/InviteEmail.jsx")
  123. await AWS.sendEmail({
  124. to: email,
  125. subject: `You've been invited to join the ${emailInfo.workspaceName} workspace on OpenCode Console`,
  126. body: render(
  127. // @ts-ignore
  128. InviteEmail({
  129. inviter: emailInfo.email,
  130. assetsUrl: `https://opencode.ai/email`,
  131. workspaceID: workspaceID,
  132. workspaceName: emailInfo.workspaceName,
  133. }),
  134. ),
  135. })
  136. } catch (e) {
  137. console.error(e)
  138. }
  139. },
  140. )
  141. export const joinInvitedWorkspaces = fn(z.void(), async () => {
  142. const account = Actor.assert("account")
  143. const invitations = await Database.use(async (tx) => {
  144. const invitations = await tx
  145. .select({
  146. id: UserTable.id,
  147. workspaceID: UserTable.workspaceID,
  148. })
  149. .from(UserTable)
  150. .where(eq(UserTable.email, account.properties.email))
  151. await tx
  152. .update(UserTable)
  153. .set({
  154. accountID: account.properties.accountID,
  155. email: null,
  156. })
  157. .where(eq(UserTable.email, account.properties.email))
  158. return invitations
  159. })
  160. await Promise.all(
  161. invitations.map((invite) =>
  162. Actor.provide(
  163. "system",
  164. {
  165. workspaceID: invite.workspaceID,
  166. },
  167. () => Key.create({ userID: invite.id, name: "Default API Key" }),
  168. ),
  169. ),
  170. )
  171. })
  172. export const update = fn(
  173. z.object({
  174. id: z.string(),
  175. role: z.enum(UserRole),
  176. monthlyLimit: z.number().nullable(),
  177. }),
  178. async ({ id, role, monthlyLimit }) => {
  179. Actor.assertAdmin()
  180. if (role === "member") assertNotSelf(id)
  181. return await Database.use((tx) =>
  182. tx
  183. .update(UserTable)
  184. .set({ role, monthlyLimit })
  185. .where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, Actor.workspace()))),
  186. )
  187. },
  188. )
  189. export const remove = fn(z.string(), async (id) => {
  190. Actor.assertAdmin()
  191. assertNotSelf(id)
  192. return await Database.use((tx) =>
  193. tx
  194. .update(UserTable)
  195. .set({
  196. timeDeleted: sql`now()`,
  197. })
  198. .where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, Actor.workspace()))),
  199. )
  200. })
  201. }