lookup-user.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import { Database, and, eq, sql } from "../src/drizzle/index.js"
  2. import { AuthTable } from "../src/schema/auth.sql.js"
  3. import { UserTable } from "../src/schema/user.sql.js"
  4. import { BillingTable, PaymentTable, SubscriptionTable, UsageTable } from "../src/schema/billing.sql.js"
  5. import { WorkspaceTable } from "../src/schema/workspace.sql.js"
  6. import { BlackData } from "../src/black.js"
  7. import { centsToMicroCents } from "../src/util/price.js"
  8. import { getWeekBounds } from "../src/util/date.js"
  9. // get input from command line
  10. const identifier = process.argv[2]
  11. if (!identifier) {
  12. console.error("Usage: bun lookup-user.ts <email|workspaceID>")
  13. process.exit(1)
  14. }
  15. if (identifier.startsWith("wrk_")) {
  16. await printWorkspace(identifier)
  17. } else {
  18. const authData = await Database.use(async (tx) =>
  19. tx.select().from(AuthTable).where(eq(AuthTable.subject, identifier)),
  20. )
  21. if (authData.length === 0) {
  22. console.error("Email not found")
  23. process.exit(1)
  24. }
  25. if (authData.length > 1) console.warn("Multiple users found for email", identifier)
  26. // Get all auth records for email
  27. const accountID = authData[0].accountID
  28. await printTable("Auth", (tx) => tx.select().from(AuthTable).where(eq(AuthTable.accountID, accountID)))
  29. // Get all workspaces for this account
  30. const users = await printTable("Workspaces", (tx) =>
  31. tx
  32. .select({
  33. userID: UserTable.id,
  34. workspaceID: UserTable.workspaceID,
  35. workspaceName: WorkspaceTable.name,
  36. role: UserTable.role,
  37. subscribed: SubscriptionTable.timeCreated,
  38. })
  39. .from(UserTable)
  40. .rightJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
  41. .leftJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id))
  42. .where(eq(UserTable.accountID, accountID))
  43. .then((rows) =>
  44. rows.map((row) => ({
  45. userID: row.userID,
  46. workspaceID: row.workspaceID,
  47. workspaceName: row.workspaceName,
  48. role: row.role,
  49. subscribed: formatDate(row.subscribed),
  50. })),
  51. ),
  52. )
  53. for (const user of users) {
  54. await printWorkspace(user.workspaceID)
  55. }
  56. }
  57. async function printWorkspace(workspaceID: string) {
  58. const workspace = await Database.use((tx) =>
  59. tx
  60. .select()
  61. .from(WorkspaceTable)
  62. .where(eq(WorkspaceTable.id, workspaceID))
  63. .then((rows) => rows[0]),
  64. )
  65. printHeader(`Workspace "${workspace.name}" (${workspace.id})`)
  66. await printTable("Users", (tx) =>
  67. tx
  68. .select({
  69. authEmail: AuthTable.subject,
  70. inviteEmail: UserTable.email,
  71. role: UserTable.role,
  72. timeSeen: UserTable.timeSeen,
  73. monthlyLimit: UserTable.monthlyLimit,
  74. monthlyUsage: UserTable.monthlyUsage,
  75. timeDeleted: UserTable.timeDeleted,
  76. fixedUsage: SubscriptionTable.fixedUsage,
  77. rollingUsage: SubscriptionTable.rollingUsage,
  78. timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
  79. timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
  80. timeSubscriptionCreated: SubscriptionTable.timeCreated,
  81. })
  82. .from(UserTable)
  83. .leftJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
  84. .leftJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id))
  85. .where(eq(UserTable.workspaceID, workspace.id))
  86. .then((rows) =>
  87. rows.map((row) => {
  88. const subStatus = getSubscriptionStatus(row)
  89. return {
  90. email: (row.timeDeleted ? "❌ " : "") + (row.authEmail ?? row.inviteEmail),
  91. role: row.role,
  92. timeSeen: formatDate(row.timeSeen),
  93. monthly: formatMonthlyUsage(row.monthlyUsage, row.monthlyLimit),
  94. subscribed: formatDate(row.timeSubscriptionCreated),
  95. subWeekly: subStatus.weekly,
  96. subRolling: subStatus.rolling,
  97. rateLimited: subStatus.rateLimited,
  98. retryIn: subStatus.retryIn,
  99. }
  100. }),
  101. ),
  102. )
  103. await printTable("Billing", (tx) =>
  104. tx
  105. .select({
  106. balance: BillingTable.balance,
  107. customerID: BillingTable.customerID,
  108. reload: BillingTable.reload,
  109. subscriptionID: BillingTable.subscriptionID,
  110. subscription: {
  111. plan: BillingTable.subscriptionPlan,
  112. booked: BillingTable.timeSubscriptionBooked,
  113. enrichment: BillingTable.subscription,
  114. },
  115. })
  116. .from(BillingTable)
  117. .where(eq(BillingTable.workspaceID, workspace.id))
  118. .then(
  119. (rows) =>
  120. rows.map((row) => ({
  121. ...row,
  122. balance: `$${(row.balance / 100000000).toFixed(2)}`,
  123. subscription: row.subscriptionID
  124. ? [
  125. `Black ${row.subscription.enrichment!.plan}`,
  126. row.subscription.enrichment!.seats > 1 ? `X ${row.subscription.enrichment!.seats} seats` : "",
  127. row.subscription.enrichment!.coupon ? `(coupon: ${row.subscription.enrichment!.coupon})` : "",
  128. `(ref: ${row.subscriptionID})`,
  129. ].join(" ")
  130. : row.subscription.booked
  131. ? `Waitlist ${row.subscription.plan} plan`
  132. : undefined,
  133. }))[0],
  134. ),
  135. )
  136. await printTable("Payments", (tx) =>
  137. tx
  138. .select({
  139. amount: PaymentTable.amount,
  140. paymentID: PaymentTable.paymentID,
  141. invoiceID: PaymentTable.invoiceID,
  142. customerID: PaymentTable.customerID,
  143. timeCreated: PaymentTable.timeCreated,
  144. timeRefunded: PaymentTable.timeRefunded,
  145. })
  146. .from(PaymentTable)
  147. .where(eq(PaymentTable.workspaceID, workspace.id))
  148. .orderBy(sql`${PaymentTable.timeCreated} DESC`)
  149. .limit(100)
  150. .then((rows) =>
  151. rows.map((row) => ({
  152. ...row,
  153. amount: `$${(row.amount / 100000000).toFixed(2)}`,
  154. paymentID: row.paymentID
  155. ? `https://dashboard.stripe.com/acct_1RszBH2StuRr0lbX/payments/${row.paymentID}`
  156. : null,
  157. })),
  158. ),
  159. )
  160. /*
  161. await printTable("Usage", (tx) =>
  162. tx
  163. .select({
  164. model: UsageTable.model,
  165. provider: UsageTable.provider,
  166. inputTokens: UsageTable.inputTokens,
  167. outputTokens: UsageTable.outputTokens,
  168. reasoningTokens: UsageTable.reasoningTokens,
  169. cacheReadTokens: UsageTable.cacheReadTokens,
  170. cacheWrite5mTokens: UsageTable.cacheWrite5mTokens,
  171. cacheWrite1hTokens: UsageTable.cacheWrite1hTokens,
  172. cost: UsageTable.cost,
  173. timeCreated: UsageTable.timeCreated,
  174. })
  175. .from(UsageTable)
  176. .where(eq(UsageTable.workspaceID, workspace.id))
  177. .orderBy(sql`${UsageTable.timeCreated} DESC`)
  178. .limit(10)
  179. .then((rows) =>
  180. rows.map((row) => ({
  181. ...row,
  182. cost: `$${(row.cost / 100000000).toFixed(2)}`,
  183. })),
  184. ),
  185. )
  186. */
  187. }
  188. function formatMicroCents(value: number | null | undefined) {
  189. if (value === null || value === undefined) return null
  190. return `$${(value / 100000000).toFixed(2)}`
  191. }
  192. function formatDate(value: Date | null | undefined) {
  193. if (!value) return null
  194. return value.toISOString().split("T")[0]
  195. }
  196. function formatMonthlyUsage(usage: number | null | undefined, limit: number | null | undefined) {
  197. const usageText = formatMicroCents(usage) ?? "$0.00"
  198. if (limit === null || limit === undefined) return `${usageText} / no limit`
  199. return `${usageText} / $${limit.toFixed(2)}`
  200. }
  201. function formatRetryTime(seconds: number) {
  202. const days = Math.floor(seconds / 86400)
  203. if (days >= 1) return `${days} day${days > 1 ? "s" : ""}`
  204. const hours = Math.floor(seconds / 3600)
  205. const minutes = Math.ceil((seconds % 3600) / 60)
  206. if (hours >= 1) return `${hours}hr ${minutes}min`
  207. return `${minutes}min`
  208. }
  209. function getSubscriptionStatus(row: {
  210. timeSubscriptionCreated: Date | null
  211. fixedUsage: number | null
  212. rollingUsage: number | null
  213. timeFixedUpdated: Date | null
  214. timeRollingUpdated: Date | null
  215. }) {
  216. if (!row.timeSubscriptionCreated) {
  217. return { weekly: null, rolling: null, rateLimited: null, retryIn: null }
  218. }
  219. const black = BlackData.get()
  220. const now = new Date()
  221. const week = getWeekBounds(now)
  222. const fixedLimit = black.fixedLimit ? centsToMicroCents(black.fixedLimit * 100) : null
  223. const rollingLimit = black.rollingLimit ? centsToMicroCents(black.rollingLimit * 100) : null
  224. const rollingWindowMs = (black.rollingWindow ?? 5) * 3600 * 1000
  225. // Calculate current weekly usage (reset if outside current week)
  226. const currentWeekly =
  227. row.fixedUsage && row.timeFixedUpdated && row.timeFixedUpdated >= week.start ? row.fixedUsage : 0
  228. // Calculate current rolling usage
  229. const windowStart = new Date(now.getTime() - rollingWindowMs)
  230. const currentRolling =
  231. row.rollingUsage && row.timeRollingUpdated && row.timeRollingUpdated >= windowStart ? row.rollingUsage : 0
  232. // Check rate limiting
  233. const isWeeklyLimited = fixedLimit !== null && currentWeekly >= fixedLimit
  234. const isRollingLimited = rollingLimit !== null && currentRolling >= rollingLimit
  235. let retryIn: string | null = null
  236. if (isWeeklyLimited) {
  237. const retryAfter = Math.ceil((week.end.getTime() - now.getTime()) / 1000)
  238. retryIn = formatRetryTime(retryAfter)
  239. } else if (isRollingLimited && row.timeRollingUpdated) {
  240. const retryAfter = Math.ceil((row.timeRollingUpdated.getTime() + rollingWindowMs - now.getTime()) / 1000)
  241. retryIn = formatRetryTime(retryAfter)
  242. }
  243. return {
  244. weekly: fixedLimit !== null ? `${formatMicroCents(currentWeekly)} / $${black.fixedLimit}` : null,
  245. rolling: rollingLimit !== null ? `${formatMicroCents(currentRolling)} / $${black.rollingLimit}` : null,
  246. rateLimited: isWeeklyLimited || isRollingLimited ? "yes" : "no",
  247. retryIn,
  248. }
  249. }
  250. function printHeader(title: string) {
  251. console.log()
  252. console.log("─".repeat(title.length))
  253. console.log(`${title}`)
  254. console.log("─".repeat(title.length))
  255. }
  256. function printTable(title: string, callback: (tx: Database.TxOrDb) => Promise<any>): Promise<any> {
  257. return Database.use(async (tx) => {
  258. const data = await callback(tx)
  259. console.log(`\n== ${title} ==`)
  260. if (data.length === 0) {
  261. console.log("(no data)")
  262. } else {
  263. console.table(data)
  264. }
  265. return data
  266. })
  267. }