user.ts 5.6 KB

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