user.ts 6.5 KB

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