user.ts 6.0 KB

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