devpush 0.0.0-stage → 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) hide show
  1. package/README.md +22 -2
  2. package/agent-release.json +37 -0
  3. package/dist/package/LICENSE.md +21 -0
  4. package/dist/package/migrate.mjs +182 -0
  5. package/dist/package/plugins.mjs +125 -0
  6. package/dist/package/vite.mjs +439 -0
  7. package/package.json +168 -3
  8. package/public/apple-touch-icon.png +0 -0
  9. package/public/favicon.svg +1 -0
  10. package/public/logo-email.png +0 -0
  11. package/public/robots.txt +3 -0
  12. package/public/social.png +0 -0
  13. package/src/cloudflare-runtime.d.ts +78 -0
  14. package/src/components/about-dialog.tsx +70 -0
  15. package/src/components/copy-button.tsx +74 -0
  16. package/src/components/logo-mark.tsx +20 -0
  17. package/src/components/pending-collection.tsx +103 -0
  18. package/src/components/resource-avatar.tsx +65 -0
  19. package/src/components/route-error-state.tsx +95 -0
  20. package/src/components/section-navigation.tsx +39 -0
  21. package/src/components/settings-layout.tsx +111 -0
  22. package/src/components/storage-capabilities.tsx +11 -0
  23. package/src/components/table-pager.tsx +101 -0
  24. package/src/components/ui/alert-dialog.tsx +180 -0
  25. package/src/components/ui/alert.tsx +78 -0
  26. package/src/components/ui/avatar.tsx +58 -0
  27. package/src/components/ui/badge.tsx +49 -0
  28. package/src/components/ui/button-group.tsx +38 -0
  29. package/src/components/ui/button.tsx +67 -0
  30. package/src/components/ui/card.tsx +82 -0
  31. package/src/components/ui/chart.tsx +307 -0
  32. package/src/components/ui/checkbox.tsx +30 -0
  33. package/src/components/ui/combobox.tsx +256 -0
  34. package/src/components/ui/dialog.tsx +163 -0
  35. package/src/components/ui/dropdown-menu.tsx +110 -0
  36. package/src/components/ui/empty.tsx +65 -0
  37. package/src/components/ui/field.tsx +192 -0
  38. package/src/components/ui/input-group.tsx +118 -0
  39. package/src/components/ui/input-otp.tsx +70 -0
  40. package/src/components/ui/input.tsx +19 -0
  41. package/src/components/ui/label.tsx +24 -0
  42. package/src/components/ui/pagination.tsx +34 -0
  43. package/src/components/ui/popover.tsx +86 -0
  44. package/src/components/ui/select.tsx +145 -0
  45. package/src/components/ui/skeleton.tsx +19 -0
  46. package/src/components/ui/sonner.tsx +47 -0
  47. package/src/components/ui/switch.tsx +31 -0
  48. package/src/components/ui/table.tsx +81 -0
  49. package/src/components/ui/textarea.tsx +18 -0
  50. package/src/components/ui/tooltip.tsx +55 -0
  51. package/src/features/admin/admin-settings.tsx +1048 -0
  52. package/src/features/auth/current-user-context-query.ts +11 -0
  53. package/src/features/auth/login-form.tsx +442 -0
  54. package/src/features/auth/sign-out-menu-item.tsx +42 -0
  55. package/src/features/deployments/deployment-actions.tsx +336 -0
  56. package/src/features/deployments/deployment-alias-badge.tsx +35 -0
  57. package/src/features/deployments/deployment-detail.tsx +596 -0
  58. package/src/features/deployments/deployment-filters.ts +66 -0
  59. package/src/features/deployments/deployment-list.tsx +465 -0
  60. package/src/features/deployments/deployment-visit-links.tsx +104 -0
  61. package/src/features/deployments/deployments-page.tsx +376 -0
  62. package/src/features/logs/log-scroll-area.tsx +69 -0
  63. package/src/features/notifications/email-preferences.tsx +232 -0
  64. package/src/features/onboarding/profile-onboarding.tsx +162 -0
  65. package/src/features/plugins/contributions.tsx +118 -0
  66. package/src/features/projects/backup-job-logs.tsx +143 -0
  67. package/src/features/projects/backup-policy-delete-dialog.tsx +82 -0
  68. package/src/features/projects/backup-restore-actions.tsx +573 -0
  69. package/src/features/projects/backup-status-icon.tsx +33 -0
  70. package/src/features/projects/backup-target-list.tsx +222 -0
  71. package/src/features/projects/backup-timestamp.tsx +32 -0
  72. package/src/features/projects/backup-volume-picker.tsx +158 -0
  73. package/src/features/projects/compose-service-input.tsx +64 -0
  74. package/src/features/projects/deploy-readiness.ts +28 -0
  75. package/src/features/projects/environment-dot.tsx +91 -0
  76. package/src/features/projects/log-keyword-filter.tsx +48 -0
  77. package/src/features/projects/manual-deploy-dialog.tsx +563 -0
  78. package/src/features/projects/pending-section.ts +26 -0
  79. package/src/features/projects/project-backup-details-pending.tsx +305 -0
  80. package/src/features/projects/project-backup-details.tsx +936 -0
  81. package/src/features/projects/project-backup-policy.tsx +644 -0
  82. package/src/features/projects/project-backup-utils.ts +190 -0
  83. package/src/features/projects/project-backups-pending.tsx +136 -0
  84. package/src/features/projects/project-backups.tsx +876 -0
  85. package/src/features/projects/project-environment-settings.tsx +1128 -0
  86. package/src/features/projects/project-heading.tsx +68 -0
  87. package/src/features/projects/project-layout.tsx +188 -0
  88. package/src/features/projects/project-logs.tsx +461 -0
  89. package/src/features/projects/project-overview.tsx +280 -0
  90. package/src/features/projects/project-route-pending.tsx +1064 -0
  91. package/src/features/projects/project-routing-settings.tsx +663 -0
  92. package/src/features/projects/project-settings-section.tsx +64 -0
  93. package/src/features/projects/project-settings-utils.ts +101 -0
  94. package/src/features/projects/project-settings.tsx +1472 -0
  95. package/src/features/projects/repository-picker-dialog.tsx +404 -0
  96. package/src/features/repositories/new-project-dialog-launcher.tsx +44 -0
  97. package/src/features/repositories/new-project-dialog.tsx +1204 -0
  98. package/src/features/repositories/new-project-pending.tsx +28 -0
  99. package/src/features/repositories/repository-list-pending.tsx +59 -0
  100. package/src/features/servers/server-availability-alert.tsx +44 -0
  101. package/src/features/servers/server-availability.ts +29 -0
  102. package/src/features/servers/server-avatar.tsx +52 -0
  103. package/src/features/servers/server-detail-pending.tsx +315 -0
  104. package/src/features/servers/server-detail.tsx +855 -0
  105. package/src/features/servers/server-metric-chart-pending.tsx +38 -0
  106. package/src/features/servers/server-metric-chart.tsx +182 -0
  107. package/src/features/servers/server-metric-format.ts +50 -0
  108. package/src/features/servers/server-metrics-pending.tsx +75 -0
  109. package/src/features/servers/server-metrics.tsx +136 -0
  110. package/src/features/servers/team-servers.tsx +432 -0
  111. package/src/features/settings/settings-route-pending.tsx +406 -0
  112. package/src/features/teams/backup-storage-settings.tsx +483 -0
  113. package/src/features/teams/navigation.ts +51 -0
  114. package/src/features/teams/new-team-dialog.tsx +132 -0
  115. package/src/features/teams/pending-section.ts +19 -0
  116. package/src/features/teams/team-header.tsx +1286 -0
  117. package/src/features/teams/team-heading.tsx +57 -0
  118. package/src/features/teams/team-home.tsx +391 -0
  119. package/src/features/teams/team-invite.tsx +155 -0
  120. package/src/features/teams/team-route-pending.tsx +505 -0
  121. package/src/features/teams/team-settings.tsx +1555 -0
  122. package/src/features/teams/use-workspace-updates.ts +126 -0
  123. package/src/features/users/api-keys-settings.tsx +544 -0
  124. package/src/features/users/api-scope-selection.tsx +269 -0
  125. package/src/features/users/connected-apps-settings.tsx +449 -0
  126. package/src/features/users/mcp-consent.tsx +220 -0
  127. package/src/features/users/profile-picture.tsx +171 -0
  128. package/src/features/users/user-settings.tsx +1064 -0
  129. package/src/lib/account-settings-schema.ts +78 -0
  130. package/src/lib/action-feedback.ts +81 -0
  131. package/src/lib/admin-search-schema.ts +8 -0
  132. package/src/lib/agent-release.d.mts +21 -0
  133. package/src/lib/agent-release.mjs +90 -0
  134. package/src/lib/allowlist-schema.ts +79 -0
  135. package/src/lib/api-key-schema.ts +176 -0
  136. package/src/lib/auth/client.ts +20 -0
  137. package/src/lib/avatar-schema.ts +11 -0
  138. package/src/lib/backup-cron.ts +118 -0
  139. package/src/lib/backup-database.ts +41 -0
  140. package/src/lib/backup-feature.ts +2 -0
  141. package/src/lib/backup-paths.ts +7 -0
  142. package/src/lib/backup-policy-schema.ts +85 -0
  143. package/src/lib/backup-resource-schema.ts +12 -0
  144. package/src/lib/backup-restore-schema.ts +15 -0
  145. package/src/lib/backup-storage-schema.ts +55 -0
  146. package/src/lib/deferred-module.ts +38 -0
  147. package/src/lib/deployment-logs-query.ts +63 -0
  148. package/src/lib/display-name.ts +7 -0
  149. package/src/lib/error-message.ts +119 -0
  150. package/src/lib/form-errors.ts +10 -0
  151. package/src/lib/internal-redirect.ts +25 -0
  152. package/src/lib/manual-deploy-options-query.ts +21 -0
  153. package/src/lib/notification-preferences.ts +43 -0
  154. package/src/lib/onboarding.ts +11 -0
  155. package/src/lib/operation-logs-query.ts +51 -0
  156. package/src/lib/page-head.ts +14 -0
  157. package/src/lib/project-context-query.ts +257 -0
  158. package/src/lib/project-creation-schema.ts +64 -0
  159. package/src/lib/project-env-schema.ts +23 -0
  160. package/src/lib/project-environments-schema.ts +56 -0
  161. package/src/lib/project-routing-schema.ts +94 -0
  162. package/src/lib/project-runtime-config.ts +131 -0
  163. package/src/lib/project-settings-schema.ts +75 -0
  164. package/src/lib/public-api-schema.ts +128 -0
  165. package/src/lib/public-error.ts +337 -0
  166. package/src/lib/query.ts +13 -0
  167. package/src/lib/repository-selection-query.ts +11 -0
  168. package/src/lib/resource-cache.ts +30 -0
  169. package/src/lib/runtime-id.ts +19 -0
  170. package/src/lib/search.ts +14 -0
  171. package/src/lib/server-context-query.ts +43 -0
  172. package/src/lib/server-name-schema.ts +11 -0
  173. package/src/lib/settings-conflict.ts +45 -0
  174. package/src/lib/settings-version.ts +7 -0
  175. package/src/lib/team-context-query.ts +126 -0
  176. package/src/lib/team-context-schema.ts +8 -0
  177. package/src/lib/team-forms-schema.ts +45 -0
  178. package/src/lib/team-role.ts +13 -0
  179. package/src/lib/team-settings-schema.ts +48 -0
  180. package/src/lib/urls.ts +3 -0
  181. package/src/lib/use-avatar-input.ts +61 -0
  182. package/src/lib/use-refresh-error.ts +18 -0
  183. package/src/lib/use-settings-version.ts +23 -0
  184. package/src/lib/utils.ts +6 -0
  185. package/src/lib/validation.ts +5 -0
  186. package/src/lib/workspace-updates.ts +53 -0
  187. package/src/package/config.ts +10 -0
  188. package/src/package/env.mjs +54 -0
  189. package/src/package/migrate-api.d.ts +5 -0
  190. package/src/package/migrate.ts +56 -0
  191. package/src/package/plugins.ts +141 -0
  192. package/src/package/sqlite.mjs +2 -0
  193. package/src/package/vite-api.d.ts +5 -0
  194. package/src/package/vite.ts +324 -0
  195. package/src/package/watch-guard.cjs +26 -0
  196. package/src/plugins/client.ts +57 -0
  197. package/src/plugins/contract.ts +188 -0
  198. package/src/plugins/default.server.ts +4 -0
  199. package/src/plugins/default.ui.ts +4 -0
  200. package/src/plugins/operations.ts +45 -0
  201. package/src/plugins/server.ts +197 -0
  202. package/src/plugins/version.ts +1 -0
  203. package/src/routeTree.gen.ts +1275 -0
  204. package/src/router.tsx +46 -0
  205. package/src/routes/$teamSlug.index.tsx +53 -0
  206. package/src/routes/$teamSlug.new-project.tsx +13 -0
  207. package/src/routes/$teamSlug.projects.$projectName.backups.$backupId.tsx +40 -0
  208. package/src/routes/$teamSlug.projects.$projectName.backups.tsx +82 -0
  209. package/src/routes/$teamSlug.projects.$projectName.deployments.$deploymentId.tsx +52 -0
  210. package/src/routes/$teamSlug.projects.$projectName.deployments.tsx +86 -0
  211. package/src/routes/$teamSlug.projects.$projectName.logs.tsx +55 -0
  212. package/src/routes/$teamSlug.projects.$projectName.settings.tsx +51 -0
  213. package/src/routes/$teamSlug.projects.$projectName.tsx +106 -0
  214. package/src/routes/$teamSlug.projects.tsx +40 -0
  215. package/src/routes/$teamSlug.servers.$serverId.tsx +31 -0
  216. package/src/routes/$teamSlug.servers.index.tsx +59 -0
  217. package/src/routes/$teamSlug.servers.tsx +7 -0
  218. package/src/routes/$teamSlug.settings.tsx +56 -0
  219. package/src/routes/$teamSlug.tsx +86 -0
  220. package/src/routes/[.]well-known.oauth-authorization-server.api.auth.ts +13 -0
  221. package/src/routes/[.]well-known.oauth-protected-resource.mcp.ts +6 -0
  222. package/src/routes/__root.tsx +155 -0
  223. package/src/routes/admin.tsx +55 -0
  224. package/src/routes/api/agents/certificates.ts +21 -0
  225. package/src/routes/api/agents/credentials/rotate.ts +22 -0
  226. package/src/routes/api/agents/heartbeat.ts +42 -0
  227. package/src/routes/api/agents/job-logs.ts +22 -0
  228. package/src/routes/api/agents/jobs/complete.ts +22 -0
  229. package/src/routes/api/agents/jobs/next.ts +29 -0
  230. package/src/routes/api/agents/jobs/progress.ts +32 -0
  231. package/src/routes/api/agents/jobs/status.ts +22 -0
  232. package/src/routes/api/agents/register.ts +45 -0
  233. package/src/routes/api/agents/runtime-logs.ts +22 -0
  234. package/src/routes/api/agents/switch.ts +25 -0
  235. package/src/routes/api/auth/$.ts +38 -0
  236. package/src/routes/api/cron/backups.ts +33 -0
  237. package/src/routes/api/cron/maintenance.ts +30 -0
  238. package/src/routes/api/github/install.callback.ts +51 -0
  239. package/src/routes/api/github/install.ts +41 -0
  240. package/src/routes/api/github/webhook.ts +11 -0
  241. package/src/routes/api/health.ts +19 -0
  242. package/src/routes/api/install/agent.$token.ts +50 -0
  243. package/src/routes/api/install/agent.binary.ts +83 -0
  244. package/src/routes/api/install/agent.checksum.ts +45 -0
  245. package/src/routes/api/install/agent.ts +26 -0
  246. package/src/routes/api/plugins/$.ts +11 -0
  247. package/src/routes/api/v1/$.ts +15 -0
  248. package/src/routes/api/workspaces/updates.ts +8 -0
  249. package/src/routes/auth.login.tsx +50 -0
  250. package/src/routes/deployments.$deploymentId.unavailable.tsx +21 -0
  251. package/src/routes/index.tsx +23 -0
  252. package/src/routes/invites.$token.tsx +35 -0
  253. package/src/routes/mcp.ts +13 -0
  254. package/src/routes/oauth.consent.tsx +54 -0
  255. package/src/routes/onboarding.tsx +37 -0
  256. package/src/routes/onboarding.workspace.tsx +44 -0
  257. package/src/routes/settings.tsx +30 -0
  258. package/src/routes/uploads/avatars/$fileName.ts +44 -0
  259. package/src/server/admin/access.ts +67 -0
  260. package/src/server/admin/functions.ts +122 -0
  261. package/src/server/admin/service.ts +260 -0
  262. package/src/server/agents/backup-state.ts +210 -0
  263. package/src/server/agents/certificates.ts +96 -0
  264. package/src/server/agents/claims.ts +116 -0
  265. package/src/server/agents/cleanup.ts +52 -0
  266. package/src/server/agents/compatibility.ts +116 -0
  267. package/src/server/agents/completion.ts +83 -0
  268. package/src/server/agents/container-cleanup-state.ts +51 -0
  269. package/src/server/agents/deployment-state.ts +111 -0
  270. package/src/server/agents/job-payloads.ts +159 -0
  271. package/src/server/agents/jobs.ts +1293 -0
  272. package/src/server/agents/protocol.ts +400 -0
  273. package/src/server/agents/reconciliation.ts +43 -0
  274. package/src/server/agents/runtime-logs.ts +337 -0
  275. package/src/server/agents/runtime-state.ts +125 -0
  276. package/src/server/api/admission.ts +12 -0
  277. package/src/server/api/authorization.ts +115 -0
  278. package/src/server/api/credentials.ts +569 -0
  279. package/src/server/api/cursors.ts +72 -0
  280. package/src/server/api/errors.ts +24 -0
  281. package/src/server/api/functions.ts +60 -0
  282. package/src/server/api/handler.ts +274 -0
  283. package/src/server/api/mutation-operations.ts +913 -0
  284. package/src/server/api/operation-context.ts +139 -0
  285. package/src/server/api/operation-contract.ts +1099 -0
  286. package/src/server/api/operations.ts +64 -0
  287. package/src/server/api/read-operations.ts +891 -0
  288. package/src/server/api/retention.ts +33 -0
  289. package/src/server/auth/access.ts +109 -0
  290. package/src/server/auth/auth-methods.ts +28 -0
  291. package/src/server/auth/better-auth.ts +518 -0
  292. package/src/server/auth/context.ts +28 -0
  293. package/src/server/auth/email-change.ts +47 -0
  294. package/src/server/auth/email-delivery.ts +51 -0
  295. package/src/server/auth/provider-identity.ts +19 -0
  296. package/src/server/auth/provider-tokens.ts +27 -0
  297. package/src/server/auth/session.server.ts +19 -0
  298. package/src/server/backups/database-commands.ts +40 -0
  299. package/src/server/backups/destination.ts +12 -0
  300. package/src/server/backups/feature.ts +6 -0
  301. package/src/server/backups/policy-validation.ts +51 -0
  302. package/src/server/backups/queue-state.ts +137 -0
  303. package/src/server/backups/service.ts +918 -0
  304. package/src/server/backups/storage-functions.ts +42 -0
  305. package/src/server/backups/storage-state.ts +67 -0
  306. package/src/server/backups/storage.ts +237 -0
  307. package/src/server/config.ts +274 -0
  308. package/src/server/db/atomic.ts +16 -0
  309. package/src/server/db/d1-connection.ts +83 -0
  310. package/src/server/db/errors.ts +17 -0
  311. package/src/server/db/index.cloudflare.ts +15 -0
  312. package/src/server/db/index.ts +35 -0
  313. package/src/server/db/libsql-connection.ts +72 -0
  314. package/src/server/db/log-schema.ts +102 -0
  315. package/src/server/db/migrate-libsql.ts +29 -0
  316. package/src/server/db/migrate-sqlite.ts +47 -0
  317. package/src/server/db/migration-history.ts +12 -0
  318. package/src/server/db/migrations-logs/0000_initial.sql +48 -0
  319. package/src/server/db/migrations-logs/0001_runtime_log_order.sql +2 -0
  320. package/src/server/db/migrations-logs/meta/0000_snapshot.json +327 -0
  321. package/src/server/db/migrations-logs/meta/0001_snapshot.json +329 -0
  322. package/src/server/db/migrations-logs/meta/_journal.json +20 -0
  323. package/src/server/db/migrations-sqlite/0000_initial.sql +894 -0
  324. package/src/server/db/migrations-sqlite/0001_agent_indexes.sql +5 -0
  325. package/src/server/db/migrations-sqlite/meta/0000_snapshot.json +5816 -0
  326. package/src/server/db/migrations-sqlite/meta/0001_snapshot.json +5841 -0
  327. package/src/server/db/migrations-sqlite/meta/_journal.json +20 -0
  328. package/src/server/db/oauth-schema.ts +216 -0
  329. package/src/server/db/remote-config.ts +47 -0
  330. package/src/server/db/schema.ts +1218 -0
  331. package/src/server/db/sqlite-connection.ts +94 -0
  332. package/src/server/db/sqlite-files.ts +27 -0
  333. package/src/server/db/sqlite-params.ts +19 -0
  334. package/src/server/db/worker-connection.ts +47 -0
  335. package/src/server/deleted-resources.ts +133 -0
  336. package/src/server/deletion-recovery.ts +272 -0
  337. package/src/server/email/layout.tsx +85 -0
  338. package/src/server/email/notification-template.tsx +62 -0
  339. package/src/server/email/otp-template.tsx +48 -0
  340. package/src/server/email/service.ts +251 -0
  341. package/src/server/email/team-invite-template.tsx +59 -0
  342. package/src/server/email/theme.ts +10 -0
  343. package/src/server/github/http.ts +14 -0
  344. package/src/server/github/install-state.ts +48 -0
  345. package/src/server/github/service.ts +831 -0
  346. package/src/server/github/webhook-handler.ts +198 -0
  347. package/src/server/github/webhooks.ts +205 -0
  348. package/src/server/http/auth.ts +11 -0
  349. package/src/server/http/error-diagnostics.ts +66 -0
  350. package/src/server/http/plugin-request.ts +15 -0
  351. package/src/server/http/proxy-request.ts +53 -0
  352. package/src/server/http/public-errors.ts +48 -0
  353. package/src/server/http/rate-limit.ts +59 -0
  354. package/src/server/http/request-body.ts +54 -0
  355. package/src/server/http/request-ip.ts +54 -0
  356. package/src/server/http/security-headers.ts +36 -0
  357. package/src/server/install/agent-binary.ts +230 -0
  358. package/src/server/install/agent-script.ts +565 -0
  359. package/src/server/install/release-binary.ts +65 -0
  360. package/src/server/logs/budget.ts +72 -0
  361. package/src/server/logs/cleanup.ts +64 -0
  362. package/src/server/logs/deployments.ts +98 -0
  363. package/src/server/logs/operations.ts +145 -0
  364. package/src/server/logs/retention.ts +39 -0
  365. package/src/server/maintenance.ts +61 -0
  366. package/src/server/mcp/connections.ts +358 -0
  367. package/src/server/mcp/functions.ts +63 -0
  368. package/src/server/mcp/handler.ts +295 -0
  369. package/src/server/mcp/oauth-discovery.cloudflare.ts +6 -0
  370. package/src/server/mcp/oauth-discovery.ts +8 -0
  371. package/src/server/mcp/oauth-provider.ts +220 -0
  372. package/src/server/mcp/urls.ts +20 -0
  373. package/src/server/notifications/functions.ts +51 -0
  374. package/src/server/notifications/observe.ts +303 -0
  375. package/src/server/notifications/outbox.ts +335 -0
  376. package/src/server/notifications/preferences.ts +131 -0
  377. package/src/server/notifications/query.ts +16 -0
  378. package/src/server/plugins/scope.ts +17 -0
  379. package/src/server/plugins/server-context.ts +157 -0
  380. package/src/server/plugins/services.server.ts +10 -0
  381. package/src/server/plugins/workspace-access.ts +74 -0
  382. package/src/server/projects/backup-policy-state.ts +68 -0
  383. package/src/server/projects/backups.ts +219 -0
  384. package/src/server/projects/compose-metadata.ts +174 -0
  385. package/src/server/projects/configuration-state.ts +90 -0
  386. package/src/server/projects/context.ts +909 -0
  387. package/src/server/projects/creation-state.ts +46 -0
  388. package/src/server/projects/deletion-state.ts +69 -0
  389. package/src/server/projects/deployment-config.ts +139 -0
  390. package/src/server/projects/deployment-logs.ts +97 -0
  391. package/src/server/projects/deployments.ts +1123 -0
  392. package/src/server/projects/domain-state.ts +71 -0
  393. package/src/server/projects/environment-branches.ts +75 -0
  394. package/src/server/projects/environment-state.ts +158 -0
  395. package/src/server/projects/environments.ts +424 -0
  396. package/src/server/projects/functions.ts +748 -0
  397. package/src/server/projects/generated-hostname.ts +77 -0
  398. package/src/server/projects/hostnames.ts +244 -0
  399. package/src/server/projects/mappers.ts +457 -0
  400. package/src/server/projects/names.ts +41 -0
  401. package/src/server/projects/project-env-vars.ts +28 -0
  402. package/src/server/projects/queue-state.ts +118 -0
  403. package/src/server/projects/repository.ts +368 -0
  404. package/src/server/projects/route-state.ts +37 -0
  405. package/src/server/projects/runtime-logs.ts +97 -0
  406. package/src/server/projects/settings-context.ts +58 -0
  407. package/src/server/projects/settings.ts +738 -0
  408. package/src/server/projects/types.ts +355 -0
  409. package/src/server/realtime/agent-events.ts +32 -0
  410. package/src/server/realtime/node-broadcaster.ts +71 -0
  411. package/src/server/realtime/node-handler.ts +55 -0
  412. package/src/server/realtime/service.ts +125 -0
  413. package/src/server/realtime/workspace-object.cloudflare.ts +62 -0
  414. package/src/server/realtime/workspace-object.ts +3 -0
  415. package/src/server/resources/color.ts +13 -0
  416. package/src/server/runtime/background.ts +21 -0
  417. package/src/server/runtime/node-asset-cache-plugin.ts +12 -0
  418. package/src/server/runtime/node-scheduler-plugin.ts +17 -0
  419. package/src/server/runtime/node-scheduler.ts +51 -0
  420. package/src/server/runtime/platform-env.cloudflare.ts +17 -0
  421. package/src/server/runtime/platform-env.ts +19 -0
  422. package/src/server/runtime/scheduled-task-error.ts +9 -0
  423. package/src/server/scheduled-tasks.ts +28 -0
  424. package/src/server/secrets/crypto.ts +56 -0
  425. package/src/server/servers/functions.ts +233 -0
  426. package/src/server/servers/lifecycle.ts +122 -0
  427. package/src/server/servers/metric-history-schema.ts +24 -0
  428. package/src/server/servers/metric-history.ts +55 -0
  429. package/src/server/servers/monitoring.ts +592 -0
  430. package/src/server/servers/service.ts +1211 -0
  431. package/src/server/servers/status.ts +32 -0
  432. package/src/server/servers/switch.ts +96 -0
  433. package/src/server/settings-version.ts +43 -0
  434. package/src/server/storage/capabilities.ts +6 -0
  435. package/src/server/storage/local-object-store.cloudflare.ts +7 -0
  436. package/src/server/storage/local-object-store.ts +50 -0
  437. package/src/server/storage/object-store.ts +160 -0
  438. package/src/server/teams/creation.ts +51 -0
  439. package/src/server/teams/delete-policy.ts +13 -0
  440. package/src/server/teams/deletion-state.ts +80 -0
  441. package/src/server/teams/functions.ts +324 -0
  442. package/src/server/teams/invitations.ts +38 -0
  443. package/src/server/teams/membership.ts +86 -0
  444. package/src/server/teams/queries.ts +261 -0
  445. package/src/server/teams/service.ts +1127 -0
  446. package/src/server/teams/types.ts +101 -0
  447. package/src/server/uploads/avatar.ts +133 -0
  448. package/src/server/users/deletion-state.ts +93 -0
  449. package/src/server/users/functions.ts +160 -0
  450. package/src/server/users/onboarding.ts +17 -0
  451. package/src/server/users/service.ts +644 -0
  452. package/src/server.ts +55 -0
  453. package/src/styles/shadcn.css +653 -0
  454. package/src/styles/theme.css +76 -0
  455. package/src/styles.css +177 -0
  456. package/wrangler.template.jsonc +37 -0
@@ -0,0 +1,1064 @@
1
+ import { AccountEmailPreferences } from '#devpush/features/notifications/email-preferences'
2
+ import {
3
+ mutationErrorOptions,
4
+ afterMutation,
5
+ } from '#devpush/lib/action-feedback'
6
+ import {
7
+ AccountSettingsContributions,
8
+ accountSettingsNavigation,
9
+ } from '#devpush/features/plugins/contributions'
10
+ import { ProfilePicture } from './profile-picture'
11
+ import { useSettingsVersion } from '#devpush/lib/use-settings-version'
12
+ import { showSettingsConflict } from '#devpush/lib/settings-conflict'
13
+ import { ConnectedAppsSettings } from './connected-apps-settings'
14
+ import { ApiKeysSettings } from './api-keys-settings'
15
+ import { useQueryClient } from '@tanstack/react-query'
16
+ import { invalidateResourceQueries } from '#devpush/lib/resource-cache'
17
+ import { useAvatarInput } from '#devpush/lib/use-avatar-input'
18
+ import {
19
+ accountSettingsSchema,
20
+ accountEmailSchema,
21
+ accountValidationErrors,
22
+ } from '#devpush/lib/account-settings-schema'
23
+ import type { AccountSettingsErrors } from '#devpush/lib/account-settings-schema'
24
+ import { Link, useRouter } from '@tanstack/react-router'
25
+ import {
26
+ ArrowUpRightIcon,
27
+ EllipsisIcon,
28
+ LoaderIcon,
29
+ LogOutIcon,
30
+ MailIcon,
31
+ Trash2Icon,
32
+ } from 'lucide-react'
33
+ import { useState } from 'react'
34
+ import { toast } from 'sonner'
35
+
36
+ import {
37
+ AlertDialog,
38
+ AlertDialogAction,
39
+ AlertDialogCancel,
40
+ AlertDialogContent,
41
+ AlertDialogDescription,
42
+ AlertDialogFooter,
43
+ AlertDialogHeader,
44
+ AlertDialogTitle,
45
+ AlertDialogTrigger,
46
+ } from '#devpush/components/ui/alert-dialog'
47
+ import { Badge } from '#devpush/components/ui/badge'
48
+ import { Button } from '#devpush/components/ui/button'
49
+ import {
50
+ Card,
51
+ CardContent,
52
+ CardDescription,
53
+ CardFooter,
54
+ CardHeader,
55
+ CardTitle,
56
+ } from '#devpush/components/ui/card'
57
+ import {
58
+ DropdownMenu,
59
+ DropdownMenuContent,
60
+ DropdownMenuItem,
61
+ DropdownMenuSeparator,
62
+ DropdownMenuTrigger,
63
+ } from '#devpush/components/ui/dropdown-menu'
64
+ import {
65
+ Field,
66
+ FieldDescription,
67
+ FieldError,
68
+ FieldGroup,
69
+ FieldLabel,
70
+ } from '#devpush/components/ui/field'
71
+ import {
72
+ Empty,
73
+ EmptyHeader,
74
+ EmptyTitle,
75
+ EmptyDescription,
76
+ } from '#devpush/components/ui/empty'
77
+ import { Input } from '#devpush/components/ui/input'
78
+ import {
79
+ Table,
80
+ TableBody,
81
+ TableCell,
82
+ TableHead,
83
+ TableHeader,
84
+ TableRow,
85
+ } from '#devpush/components/ui/table'
86
+ import {
87
+ Tooltip,
88
+ TooltipContent,
89
+ TooltipTrigger,
90
+ } from '#devpush/components/ui/tooltip'
91
+ import { authClient } from '#devpush/lib/auth/client'
92
+ import { getPublicErrorMessage } from '#devpush/lib/public-error'
93
+ import { ResourceAvatar } from '#devpush/components/resource-avatar'
94
+ import { StandaloneSettingsLayout } from '#devpush/components/settings-layout'
95
+ import type { AuthUserContext } from '#devpush/server/auth/context'
96
+ import {
97
+ deleteUserAccountAction,
98
+ disconnectAuthAccountAction,
99
+ leaveTeamAction,
100
+ requestEmailChangeAction,
101
+ updateUserGeneralAction,
102
+ } from '#devpush/server/users/functions'
103
+ import type {
104
+ AuthAccountSummary,
105
+ UserTeamSummary,
106
+ } from '#devpush/server/users/service'
107
+
108
+ type UserSettingsContext = Omit<AuthUserContext, 'teams'> & {
109
+ accounts: Array<AuthAccountSummary>
110
+ authMethods: {
111
+ email: boolean
112
+ github: boolean
113
+ google: boolean
114
+ }
115
+ teams: Array<UserTeamSummary>
116
+ }
117
+
118
+ type SocialProvider = 'github' | 'google'
119
+
120
+ export function UserSettings({ context }: { context: UserSettingsContext }) {
121
+ const router = useRouter()
122
+ const queryClient = useQueryClient()
123
+ const { accounts, defaultTeam, teams, user } = context
124
+ const [savedGeneral, setSavedGeneral] = useState(user)
125
+ const [savedEmail, setSavedEmail] = useState(user.email)
126
+ const [generalErrors, setGeneralErrors] = useState<AccountSettingsErrors>({})
127
+ const [name, setName] = useState(user.name)
128
+ const [username, setUsername] = useState(user.username)
129
+ const {
130
+ dataUrl: avatarDataUrl,
131
+ setDataUrl: setAvatarDataUrl,
132
+ read: readAvatar,
133
+ cancelRead: cancelAvatarRead,
134
+ pending: isReadingAvatar,
135
+ } = useAvatarInput()
136
+ const [deleteAvatar, setDeleteAvatar] = useState(false)
137
+ const [newEmail, setNewEmail] = useState(user.email)
138
+ const [isSavingGeneral, setIsSavingGeneral] = useState(false)
139
+ const [emailError, setEmailError] = useState<string | null>(null)
140
+ const [isSavingEmail, setIsSavingEmail] = useState(false)
141
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
142
+ const [accountDeleted, setAccountDeleted] = useState(false)
143
+ const [leavingTeamId, setLeavingTeamId] = useState<string | null>(null)
144
+ const [isDeletingAccount, setIsDeletingAccount] = useState(false)
145
+ const [deleteAccountConfirmation, setDeleteAccountConfirmation] = useState('')
146
+ const [busyId, setBusyId] = useState<string | null>(null)
147
+ const [pendingProvider, setPendingProvider] = useState<SocialProvider | null>(
148
+ null,
149
+ )
150
+ const [accountToDisconnect, setAccountToDisconnect] =
151
+ useState<AuthAccountSummary | null>(null)
152
+
153
+ const avatarUrl = deleteAvatar ? null : (avatarDataUrl ?? user.image)
154
+ const generalDirty =
155
+ isReadingAvatar ||
156
+ name.trim() !== savedGeneral.name ||
157
+ username.trim() !== savedGeneral.username ||
158
+ Boolean(avatarDataUrl) ||
159
+ deleteAvatar
160
+ const emailDirty = newEmail.trim().toLowerCase() !== savedEmail.toLowerCase()
161
+ const generalVersion = useSettingsVersion({
162
+ version: user.settingsVersion,
163
+ dirty: generalDirty,
164
+ saving: isSavingGeneral,
165
+ refresh: () => {
166
+ setName(user.name)
167
+ setUsername(user.username)
168
+ setSavedGeneral(user)
169
+ setGeneralErrors({})
170
+ },
171
+ })
172
+ const emailVersion = useSettingsVersion({
173
+ version: user.settingsVersion,
174
+ dirty: emailDirty,
175
+ saving: isSavingEmail,
176
+ refresh: () => {
177
+ setNewEmail(user.email)
178
+ setSavedEmail(user.email)
179
+ setEmailError(null)
180
+ },
181
+ })
182
+ const socialProviders = ['github', 'google'] as const
183
+ const accountsByProvider = new Map(
184
+ accounts.map((account) => [account.providerId, account]),
185
+ )
186
+
187
+ async function chooseAvatar(file: File | undefined) {
188
+ if (!file) return
189
+ cancelAvatarRead()
190
+ setGeneralErrors((current) => ({ ...current, avatarDataUrl: undefined }))
191
+
192
+ if (!['image/png', 'image/jpeg', 'image/webp'].includes(file.type)) {
193
+ setGeneralErrors((current) => ({
194
+ ...current,
195
+ avatarDataUrl: 'Upload a PNG, JPEG, or WebP image.',
196
+ }))
197
+ return
198
+ }
199
+
200
+ if (file.size > 2 * 1024 * 1024) {
201
+ setGeneralErrors((current) => ({
202
+ ...current,
203
+ avatarDataUrl: 'Avatars must be 2 MB or smaller.',
204
+ }))
205
+ return
206
+ }
207
+
208
+ try {
209
+ const dataUrl = await readAvatar(file)
210
+ if (dataUrl === null) return
211
+ setAvatarDataUrl(dataUrl)
212
+ setDeleteAvatar(false)
213
+ } catch {
214
+ setGeneralErrors((current) => ({
215
+ ...current,
216
+ avatarDataUrl:
217
+ 'Unable to read this image. Please choose it again or try another file.',
218
+ }))
219
+ }
220
+ }
221
+
222
+ async function saveGeneral() {
223
+ if (isSavingGeneral || isReadingAvatar || !generalDirty) return
224
+ const parsed = accountSettingsSchema.safeParse({
225
+ settingsVersion: generalVersion.current,
226
+ name,
227
+ username,
228
+ avatarDataUrl,
229
+ deleteAvatar,
230
+ })
231
+ if (!parsed.success) {
232
+ const errors = accountValidationErrors(parsed.error.issues)
233
+ if (errors.form) toast.error(errors.form)
234
+ setGeneralErrors(errors)
235
+ const field = errors.name
236
+ ? 'name'
237
+ : errors.username
238
+ ? 'username'
239
+ : 'avatar-button'
240
+ document.getElementById(`account-${field}`)?.focus()
241
+ return
242
+ }
243
+ setGeneralErrors({})
244
+ setIsSavingGeneral(true)
245
+
246
+ try {
247
+ const result = await updateUserGeneralAction({ data: parsed.data })
248
+ if (result.fieldErrors) {
249
+ if (result.fieldErrors.form) toast.error(result.fieldErrors.form)
250
+ setGeneralErrors(result.fieldErrors)
251
+ return
252
+ }
253
+ generalVersion.current = result.user.settingsVersion
254
+ setSavedGeneral((current) => ({ ...current, ...result.user }))
255
+ setName(result.user.name)
256
+ setUsername(result.user.username)
257
+ setAvatarDataUrl(null)
258
+ setDeleteAvatar(false)
259
+ toast.success('Account settings saved.')
260
+ await afterMutation(async () => {
261
+ await invalidateResourceQueries(queryClient)
262
+ await router.invalidate()
263
+ }, 'The action completed, but refreshing the page failed. Please reload.')
264
+ } catch (error) {
265
+ if (showSettingsConflict(error)) return
266
+ toast.error(
267
+ getPublicErrorMessage(
268
+ error,
269
+ 'Unable to save account. Please try again.',
270
+ ),
271
+ ...mutationErrorOptions(error),
272
+ )
273
+ } finally {
274
+ setIsSavingGeneral(false)
275
+ }
276
+ }
277
+
278
+ async function requestEmailChange() {
279
+ if (isSavingEmail || !emailDirty) return
280
+ const parsed = accountEmailSchema.safeParse({
281
+ newEmail,
282
+ settingsVersion: emailVersion.current,
283
+ })
284
+ if (!parsed.success) {
285
+ setEmailError(
286
+ parsed.error.issues[0]?.message ?? 'Enter a valid email address.',
287
+ )
288
+ document.getElementById('account-email')?.focus()
289
+ return
290
+ }
291
+ setEmailError(null)
292
+
293
+ setIsSavingEmail(true)
294
+
295
+ try {
296
+ const result = await requestEmailChangeAction({ data: parsed.data })
297
+ emailVersion.current = result.settingsVersion
298
+ toast.success('Verification email sent.')
299
+ setNewEmail(user.email)
300
+ await afterMutation(async () => {
301
+ await invalidateResourceQueries(queryClient)
302
+ await router.invalidate()
303
+ }, 'The action completed, but refreshing the page failed. Please reload.')
304
+ } catch (error) {
305
+ if (showSettingsConflict(error)) return
306
+ toast.error(
307
+ getPublicErrorMessage(
308
+ error,
309
+ 'Unable to send the verification email. Please try again.',
310
+ ),
311
+ ...mutationErrorOptions(error),
312
+ )
313
+ } finally {
314
+ setIsSavingEmail(false)
315
+ }
316
+ }
317
+
318
+ async function disconnectAccount(account: AuthAccountSummary) {
319
+ if (busyId) return
320
+
321
+ setBusyId(account.id)
322
+
323
+ try {
324
+ await disconnectAuthAccountAction({
325
+ data: {
326
+ providerId: account.providerId,
327
+ accountId: account.accountId,
328
+ },
329
+ })
330
+ setAccountToDisconnect(null)
331
+ toast.success('Sign-in method disconnected.')
332
+ await afterMutation(async () => {
333
+ await invalidateResourceQueries(queryClient)
334
+ await router.invalidate()
335
+ }, 'The action completed, but refreshing the page failed. Please reload.')
336
+ } catch (error) {
337
+ if (showSettingsConflict(error)) return
338
+ toast.error(
339
+ getPublicErrorMessage(error, 'Unable to disconnect account.'),
340
+ ...mutationErrorOptions(error),
341
+ )
342
+ } finally {
343
+ setBusyId(null)
344
+ }
345
+ }
346
+
347
+ async function connectProvider(provider: SocialProvider) {
348
+ setPendingProvider(provider)
349
+
350
+ try {
351
+ const result = await authClient.linkSocial({
352
+ provider,
353
+ callbackURL: '/settings#authentication',
354
+ })
355
+
356
+ if (result.error) {
357
+ toast.error(
358
+ getPublicErrorMessage(result.error, `Unable to connect ${provider}.`),
359
+ ...mutationErrorOptions(result.error),
360
+ )
361
+ setPendingProvider(null)
362
+ }
363
+ } catch {
364
+ toast.error(`Unable to connect ${provider}.`)
365
+ setPendingProvider(null)
366
+ }
367
+ }
368
+
369
+ async function leave(teamId: string) {
370
+ if (busyId) return
371
+
372
+ setBusyId(teamId)
373
+
374
+ try {
375
+ await leaveTeamAction({ data: { teamId } })
376
+ setLeavingTeamId(null)
377
+ toast.success('You left the team.')
378
+ await afterMutation(async () => {
379
+ await invalidateResourceQueries(queryClient)
380
+ await router.invalidate()
381
+ }, 'The action completed, but refreshing the page failed. Please reload.')
382
+ } catch (error) {
383
+ if (showSettingsConflict(error)) return
384
+ toast.error(
385
+ getPublicErrorMessage(error, 'Unable to leave team.'),
386
+ ...mutationErrorOptions(error),
387
+ )
388
+ } finally {
389
+ setBusyId(null)
390
+ }
391
+ }
392
+
393
+ async function deleteAccount() {
394
+ if (
395
+ isDeletingAccount ||
396
+ accountDeleted ||
397
+ deleteAccountConfirmation !== user.email
398
+ )
399
+ return
400
+
401
+ setIsDeletingAccount(true)
402
+
403
+ try {
404
+ await deleteUserAccountAction({
405
+ data: { confirmationName: deleteAccountConfirmation },
406
+ })
407
+ setAccountDeleted(true)
408
+ setDeleteDialogOpen(false)
409
+ queryClient.clear()
410
+ await authClient.signOut().catch(() => {})
411
+ toast.success('Account deleted.')
412
+ await afterMutation(async () => {
413
+ await router.navigate({ to: '/auth/login' })
414
+ }, 'The action completed, but opening the next page failed. Please reload.')
415
+ } catch (error) {
416
+ if (showSettingsConflict(error)) return
417
+ toast.error(
418
+ getPublicErrorMessage(error, 'Unable to delete account.'),
419
+ ...mutationErrorOptions(error),
420
+ )
421
+ } finally {
422
+ setIsDeletingAccount(false)
423
+ }
424
+ }
425
+
426
+ return (
427
+ <StandaloneSettingsLayout
428
+ title="Account"
429
+ navigation={[
430
+ { href: '#general', label: 'General' },
431
+ { href: '#email-preferences', label: 'Email preferences' },
432
+ { href: '#authentication', label: 'Authentication' },
433
+ { href: '#api-keys', label: 'API keys' },
434
+ { href: '#connected-apps', label: 'Connected apps' },
435
+ { href: '#teams', label: 'Teams' },
436
+ ...accountSettingsNavigation,
437
+ { href: '#danger', label: 'Danger', destructive: true },
438
+ ]}
439
+ >
440
+ <Card id="general" className="scroll-mt-8">
441
+ <CardHeader className="border-b">
442
+ <CardTitle>General</CardTitle>
443
+ <CardDescription>General settings for your account.</CardDescription>
444
+ </CardHeader>
445
+ <CardContent>
446
+ <form
447
+ id="account-general-form"
448
+ noValidate
449
+ onSubmit={(event) => {
450
+ event.preventDefault()
451
+ void saveGeneral()
452
+ }}
453
+ >
454
+ <FieldGroup>
455
+ <Field data-invalid={Boolean(generalErrors.avatarDataUrl)}>
456
+ <FieldLabel htmlFor="account-avatar-button">Picture</FieldLabel>
457
+ <ProfilePicture
458
+ key={savedGeneral.settingsVersion}
459
+ id="account-avatar-button"
460
+ image={avatarUrl}
461
+ name={name || username}
462
+ email={user.email}
463
+ disabled={isSavingGeneral}
464
+ processing={isReadingAvatar}
465
+ onFile={chooseAvatar}
466
+ onChange={(value) => {
467
+ setAvatarDataUrl(value === user.image ? null : value)
468
+ setDeleteAvatar(value === null)
469
+ setGeneralErrors((current) => ({
470
+ ...current,
471
+ avatarDataUrl: undefined,
472
+ }))
473
+ }}
474
+ />
475
+ {isReadingAvatar ? (
476
+ <p role="status" className="text-sm text-muted-foreground">
477
+ Reading image…
478
+ </p>
479
+ ) : null}
480
+ <FieldError id="account-avatar-error">
481
+ {generalErrors.avatarDataUrl}
482
+ </FieldError>
483
+ </Field>
484
+ <Field data-invalid={Boolean(generalErrors.name)}>
485
+ <FieldLabel htmlFor="account-name">Name</FieldLabel>
486
+ <Input
487
+ id="account-name"
488
+ disabled={isSavingGeneral}
489
+ required
490
+ aria-invalid={Boolean(generalErrors.name)}
491
+ aria-describedby="account-name-error"
492
+ value={name}
493
+ onChange={(event) => {
494
+ setName(event.target.value)
495
+ setGeneralErrors((current) => ({
496
+ ...current,
497
+ name: undefined,
498
+ }))
499
+ }}
500
+ />
501
+ <FieldError id="account-name-error">
502
+ {generalErrors.name}
503
+ </FieldError>
504
+ </Field>
505
+ <Field data-invalid={Boolean(generalErrors.username)}>
506
+ <FieldLabel htmlFor="account-username">Username</FieldLabel>
507
+ <Input
508
+ id="account-username"
509
+ disabled={isSavingGeneral}
510
+ required
511
+ aria-invalid={Boolean(generalErrors.username)}
512
+ aria-describedby="account-username-error"
513
+ value={username}
514
+ onChange={(event) => {
515
+ setUsername(event.target.value)
516
+ setGeneralErrors((current) => ({
517
+ ...current,
518
+ username: undefined,
519
+ }))
520
+ }}
521
+ />
522
+ <FieldError id="account-username-error">
523
+ {generalErrors.username}
524
+ </FieldError>
525
+ </Field>
526
+ </FieldGroup>
527
+ </form>
528
+ <div className="mt-6">
529
+ <form
530
+ id="account-email-form"
531
+ noValidate
532
+ onSubmit={(event) => {
533
+ event.preventDefault()
534
+ void requestEmailChange()
535
+ }}
536
+ >
537
+ <Field data-invalid={Boolean(emailError)}>
538
+ <FieldLabel htmlFor="account-email">Email</FieldLabel>
539
+ <Input
540
+ aria-invalid={Boolean(emailError)}
541
+ id="account-email"
542
+ type="email"
543
+ required
544
+ maxLength={320}
545
+ disabled={isSavingEmail}
546
+ aria-describedby="account-email-error"
547
+ value={newEmail}
548
+ onChange={(event) => {
549
+ setNewEmail(event.target.value)
550
+ setEmailError(null)
551
+ }}
552
+ />
553
+ <FieldDescription>
554
+ Verification is required before the new email is used for sign
555
+ in.
556
+ </FieldDescription>
557
+ <FieldError id="account-email-error">{emailError}</FieldError>
558
+ </Field>
559
+ {emailDirty ? (
560
+ <div className="mt-3 flex justify-end gap-2">
561
+ <Button
562
+ variant="ghost"
563
+ type="button"
564
+ disabled={isSavingEmail}
565
+ onClick={() => {
566
+ setNewEmail(user.email)
567
+ setEmailError(null)
568
+ }}
569
+ >
570
+ Cancel
571
+ </Button>
572
+ <Button type="submit" disabled={isSavingEmail}>
573
+ {isSavingEmail ? (
574
+ <LoaderIcon
575
+ data-icon="inline-start"
576
+ className="animate-spin"
577
+ />
578
+ ) : null}
579
+ Verify email
580
+ </Button>
581
+ </div>
582
+ ) : null}
583
+ </form>
584
+ </div>
585
+ </CardContent>
586
+ <CardFooter className="justify-end gap-2">
587
+ <Button
588
+ variant="ghost"
589
+ disabled={!generalDirty || isSavingGeneral}
590
+ onClick={() => {
591
+ setGeneralErrors({})
592
+ generalVersion.current = user.settingsVersion
593
+ setSavedGeneral(user)
594
+ setName(user.name)
595
+ setUsername(user.username)
596
+ setAvatarDataUrl(null)
597
+ setDeleteAvatar(false)
598
+ }}
599
+ >
600
+ Cancel
601
+ </Button>
602
+ <Button
603
+ disabled={!generalDirty || isSavingGeneral || isReadingAvatar}
604
+ type="submit"
605
+ form="account-general-form"
606
+ >
607
+ {isSavingGeneral ? (
608
+ <LoaderIcon data-icon="inline-start" className="animate-spin" />
609
+ ) : null}
610
+ Save
611
+ </Button>
612
+ </CardFooter>
613
+ </Card>
614
+
615
+ <AccountEmailPreferences userId={user.id} />
616
+ <Card id="authentication" className="scroll-mt-8">
617
+ <CardHeader className="border-b">
618
+ <CardTitle>Authentication</CardTitle>
619
+ <CardDescription>
620
+ Manage the ways you can sign in to your account.
621
+ </CardDescription>
622
+ </CardHeader>
623
+ <CardContent>
624
+ <div className="overflow-hidden rounded-xl border bg-card text-card-foreground">
625
+ {context.authMethods.email ? (
626
+ <div className="flex h-11 items-center gap-4 border-b px-4 text-sm last:border-b-0">
627
+ <MailIcon className="size-4 shrink-0" />
628
+ <div className="grid min-w-0 flex-1 grid-cols-[120px_1fr] items-center gap-4">
629
+ <span className="font-medium">Email</span>
630
+ <span className="truncate text-muted-foreground">
631
+ {user.email}
632
+ </span>
633
+ </div>
634
+ </div>
635
+ ) : null}
636
+ {socialProviders.map((provider) => {
637
+ const account = accountsByProvider.get(provider)
638
+ const accountIdentity = account
639
+ ? getAccountIdentity(account)
640
+ : null
641
+ const isConfigured = context.authMethods[provider]
642
+ const isConnected = Boolean(account)
643
+
644
+ return (
645
+ <div
646
+ className="flex h-11 items-center gap-4 border-b px-4 text-sm last:border-b-0"
647
+ key={provider}
648
+ >
649
+ <ProviderIcon provider={provider} connected={isConnected} />
650
+ <div
651
+ className={`grid min-w-0 flex-1 grid-cols-[120px_1fr] items-center gap-4 ${
652
+ isConnected ? '' : 'text-muted-foreground'
653
+ }`}
654
+ >
655
+ <span className="font-medium capitalize">{provider}</span>
656
+ <span className="truncate text-muted-foreground">
657
+ {accountIdentity ?? 'Not connected'}
658
+ </span>
659
+ </div>
660
+ {account ? (
661
+ <DropdownMenu>
662
+ <DropdownMenuTrigger asChild>
663
+ <Button
664
+ variant="ghost"
665
+ size="icon"
666
+ disabled={busyId === account.id}
667
+ aria-label={`${provider} options`}
668
+ >
669
+ {busyId === account.id ? (
670
+ <LoaderIcon className="animate-spin" />
671
+ ) : (
672
+ <EllipsisIcon />
673
+ )}
674
+ </Button>
675
+ </DropdownMenuTrigger>
676
+ <DropdownMenuContent align="end" className="w-auto">
677
+ {account.managementUrl ? (
678
+ <DropdownMenuItem asChild>
679
+ <a
680
+ href={account.managementUrl}
681
+ target="_blank"
682
+ rel="noreferrer"
683
+ className="gap-2 whitespace-nowrap"
684
+ >
685
+ Manage account
686
+ <ArrowUpRightIcon className="ml-auto size-3 text-muted-foreground opacity-50" />
687
+ </a>
688
+ </DropdownMenuItem>
689
+ ) : null}
690
+ {account.managementUrl ? (
691
+ <DropdownMenuSeparator />
692
+ ) : null}
693
+ <DropdownMenuItem
694
+ variant="destructive"
695
+ onSelect={() => {
696
+ setAccountToDisconnect(account)
697
+ }}
698
+ >
699
+ Disconnect
700
+ </DropdownMenuItem>
701
+ </DropdownMenuContent>
702
+ </DropdownMenu>
703
+ ) : (
704
+ <Button
705
+ variant="outline"
706
+ size="sm"
707
+ disabled={!isConfigured || pendingProvider === provider}
708
+ onClick={() => void connectProvider(provider)}
709
+ >
710
+ {pendingProvider === provider ? (
711
+ <LoaderIcon
712
+ data-icon="inline-start"
713
+ className="animate-spin"
714
+ />
715
+ ) : null}
716
+ Connect
717
+ </Button>
718
+ )}
719
+ </div>
720
+ )
721
+ })}
722
+ </div>
723
+ <AlertDialog
724
+ open={Boolean(accountToDisconnect)}
725
+ onOpenChange={(open) => {
726
+ if (!open && !busyId) {
727
+ setAccountToDisconnect(null)
728
+ }
729
+ }}
730
+ >
731
+ <AlertDialogContent>
732
+ <AlertDialogHeader>
733
+ <AlertDialogTitle>
734
+ Disconnect {accountToDisconnect?.providerId}?
735
+ </AlertDialogTitle>
736
+ <AlertDialogDescription>
737
+ You will no longer be able to sign in with this provider
738
+ unless you connect it again.
739
+ </AlertDialogDescription>
740
+ </AlertDialogHeader>
741
+ <AlertDialogFooter>
742
+ <AlertDialogCancel
743
+ disabled={busyId === accountToDisconnect?.id}
744
+ >
745
+ Cancel
746
+ </AlertDialogCancel>
747
+ <AlertDialogAction
748
+ variant="destructive"
749
+ disabled={busyId === accountToDisconnect?.id}
750
+ onClick={(event) => {
751
+ event.preventDefault()
752
+ if (accountToDisconnect) {
753
+ void disconnectAccount(accountToDisconnect)
754
+ }
755
+ }}
756
+ >
757
+ {busyId === accountToDisconnect?.id ? (
758
+ <LoaderIcon className="animate-spin" />
759
+ ) : null}
760
+ Disconnect
761
+ </AlertDialogAction>
762
+ </AlertDialogFooter>
763
+ </AlertDialogContent>
764
+ </AlertDialog>
765
+ </CardContent>
766
+ </Card>
767
+
768
+ <ApiKeysSettings userId={user.id} />
769
+ <ConnectedAppsSettings userId={user.id} />
770
+
771
+ <Card id="teams" className="scroll-mt-8">
772
+ <CardHeader className="border-b">
773
+ <CardTitle>Teams</CardTitle>
774
+ <CardDescription>
775
+ Manage the teams you are a member of.
776
+ </CardDescription>
777
+ </CardHeader>
778
+ <CardContent>
779
+ {teams.length === 0 ? (
780
+ <Empty className="min-h-40">
781
+ <EmptyHeader>
782
+ <EmptyTitle>No teams.</EmptyTitle>
783
+ <EmptyDescription>
784
+ Join or create a team to collaborate on projects.
785
+ </EmptyDescription>
786
+ </EmptyHeader>
787
+ </Empty>
788
+ ) : (
789
+ <div className="overflow-hidden rounded-xl border bg-card text-card-foreground">
790
+ <Table>
791
+ <TableHeader>
792
+ <TableRow>
793
+ <TableHead className="pl-4">Name</TableHead>
794
+ <TableHead>Role</TableHead>
795
+ <TableHead className="w-10">
796
+ <span className="sr-only">Actions</span>
797
+ </TableHead>
798
+ </TableRow>
799
+ </TableHeader>
800
+ <TableBody>
801
+ {teams.map((team) => (
802
+ <TableRow key={team.id}>
803
+ <TableCell className="pl-4">
804
+ <div className="flex items-center gap-3">
805
+ <ResourceAvatar
806
+ name={team.name}
807
+ src={team.image ?? undefined}
808
+ color={team.color}
809
+ size="sm"
810
+ />
811
+ <Link
812
+ to="/$teamSlug"
813
+ params={{ teamSlug: team.slug }}
814
+ className="font-medium"
815
+ >
816
+ {team.name}
817
+ </Link>
818
+ {team.id === defaultTeam.id ? (
819
+ <Badge variant="secondary">Default</Badge>
820
+ ) : null}
821
+ </div>
822
+ </TableCell>
823
+ <TableCell className="capitalize text-muted-foreground">
824
+ {team.role}
825
+ </TableCell>
826
+ <TableCell className="text-right">
827
+ <AlertDialog
828
+ open={leavingTeamId === team.id}
829
+ onOpenChange={(open) => {
830
+ if (!busyId) {
831
+ setLeavingTeamId(open ? team.id : null)
832
+ }
833
+ }}
834
+ >
835
+ <Tooltip>
836
+ <TooltipTrigger asChild>
837
+ <AlertDialogTrigger asChild>
838
+ <Button
839
+ variant="ghost"
840
+ size="icon"
841
+ disabled={
842
+ team.isDefault || busyId === team.id
843
+ }
844
+ aria-label={`Leave ${team.name}`}
845
+ >
846
+ {busyId === team.id ? (
847
+ <LoaderIcon className="animate-spin" />
848
+ ) : (
849
+ <LogOutIcon />
850
+ )}
851
+ </Button>
852
+ </AlertDialogTrigger>
853
+ </TooltipTrigger>
854
+ <TooltipContent>Leave team</TooltipContent>
855
+ </Tooltip>
856
+ <AlertDialogContent>
857
+ <AlertDialogHeader>
858
+ <AlertDialogTitle>
859
+ Leave {team.name}?
860
+ </AlertDialogTitle>
861
+ <AlertDialogDescription>
862
+ You will lose access to this team and its
863
+ projects.
864
+ </AlertDialogDescription>
865
+ </AlertDialogHeader>
866
+ <AlertDialogFooter>
867
+ <AlertDialogCancel disabled={busyId === team.id}>
868
+ Cancel
869
+ </AlertDialogCancel>
870
+ <AlertDialogAction
871
+ variant="destructive"
872
+ disabled={busyId === team.id}
873
+ onClick={(event) => {
874
+ event.preventDefault()
875
+ void leave(team.id)
876
+ }}
877
+ >
878
+ {busyId === team.id ? (
879
+ <LoaderIcon className="animate-spin" />
880
+ ) : null}
881
+ Leave team
882
+ </AlertDialogAction>
883
+ </AlertDialogFooter>
884
+ </AlertDialogContent>
885
+ </AlertDialog>
886
+ </TableCell>
887
+ </TableRow>
888
+ ))}
889
+ </TableBody>
890
+ </Table>
891
+ </div>
892
+ )}
893
+ </CardContent>
894
+ </Card>
895
+
896
+ <AccountSettingsContributions userId={user.id} />
897
+
898
+ <Card id="danger" className="scroll-mt-8">
899
+ <CardHeader className="border-b">
900
+ <CardTitle>Danger</CardTitle>
901
+ <CardDescription>
902
+ Permanently delete your account. Teams where you are the only member
903
+ are deleted too.
904
+ </CardDescription>
905
+ </CardHeader>
906
+ <CardContent>
907
+ <AlertDialog
908
+ open={deleteDialogOpen}
909
+ onOpenChange={(open) => {
910
+ if (!isDeletingAccount && !accountDeleted) {
911
+ setDeleteDialogOpen(open)
912
+ }
913
+ }}
914
+ >
915
+ <AlertDialogTrigger asChild>
916
+ <Button
917
+ variant="destructive"
918
+ disabled={isDeletingAccount || accountDeleted}
919
+ onClick={() => setDeleteAccountConfirmation('')}
920
+ >
921
+ {isDeletingAccount ? (
922
+ <LoaderIcon
923
+ data-icon="inline-start"
924
+ className="animate-spin"
925
+ />
926
+ ) : (
927
+ <Trash2Icon data-icon="inline-start" />
928
+ )}
929
+ Delete account
930
+ </Button>
931
+ </AlertDialogTrigger>
932
+ <AlertDialogContent>
933
+ <AlertDialogHeader>
934
+ <AlertDialogTitle>Delete your account?</AlertDialogTitle>
935
+ <AlertDialogDescription>
936
+ This permanently deletes your account. Teams where you are the
937
+ only member are deleted too. If you are the only owner of a
938
+ team with other members, transfer ownership before deleting
939
+ your account. {`Type "${user.email}" to confirm.`}
940
+ </AlertDialogDescription>
941
+ </AlertDialogHeader>
942
+ <Field>
943
+ <Input
944
+ aria-label={`Type ${user.email} to confirm`}
945
+ id="delete-account-confirmation"
946
+ required
947
+ disabled={isDeletingAccount || accountDeleted}
948
+ placeholder={user.email}
949
+ value={deleteAccountConfirmation}
950
+ onChange={(event) =>
951
+ setDeleteAccountConfirmation(event.target.value)
952
+ }
953
+ onKeyDown={(event) => {
954
+ if (
955
+ event.key === 'Enter' &&
956
+ !isDeletingAccount &&
957
+ deleteAccountConfirmation === user.email
958
+ ) {
959
+ event.preventDefault()
960
+ void deleteAccount()
961
+ }
962
+ }}
963
+ />
964
+ </Field>
965
+ <AlertDialogFooter>
966
+ <AlertDialogCancel
967
+ disabled={isDeletingAccount || accountDeleted}
968
+ >
969
+ Cancel
970
+ </AlertDialogCancel>
971
+ <AlertDialogAction
972
+ variant="destructive"
973
+ disabled={
974
+ isDeletingAccount ||
975
+ deleteAccountConfirmation !== user.email
976
+ }
977
+ onClick={(event) => {
978
+ event.preventDefault()
979
+ void deleteAccount()
980
+ }}
981
+ >
982
+ {isDeletingAccount ? (
983
+ <LoaderIcon className="animate-spin" />
984
+ ) : null}
985
+ Delete account
986
+ </AlertDialogAction>
987
+ </AlertDialogFooter>
988
+ </AlertDialogContent>
989
+ </AlertDialog>
990
+ </CardContent>
991
+ </Card>
992
+ </StandaloneSettingsLayout>
993
+ )
994
+ }
995
+
996
+ function getAccountIdentity(account: AuthAccountSummary) {
997
+ return (
998
+ account.providerAccountUsername ??
999
+ account.providerAccountEmail ??
1000
+ account.providerAccountName ??
1001
+ account.accountId
1002
+ )
1003
+ }
1004
+
1005
+ function ProviderIcon({
1006
+ connected,
1007
+ provider,
1008
+ }: {
1009
+ connected: boolean
1010
+ provider: SocialProvider
1011
+ }) {
1012
+ const className = connected
1013
+ ? 'size-4 shrink-0'
1014
+ : 'size-4 shrink-0 opacity-45 grayscale'
1015
+
1016
+ if (provider === 'github') {
1017
+ return <GithubIcon className={className} />
1018
+ }
1019
+
1020
+ return <GoogleIcon className={className} />
1021
+ }
1022
+
1023
+ function GithubIcon({ className }: { className?: string }) {
1024
+ return (
1025
+ <svg
1026
+ role="img"
1027
+ fill="currentColor"
1028
+ viewBox="0 0 24 24"
1029
+ xmlns="http://www.w3.org/2000/svg"
1030
+ className={className}
1031
+ aria-hidden="true"
1032
+ >
1033
+ <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
1034
+ </svg>
1035
+ )
1036
+ }
1037
+
1038
+ function GoogleIcon({ className }: { className?: string }) {
1039
+ return (
1040
+ <svg
1041
+ xmlns="http://www.w3.org/2000/svg"
1042
+ viewBox="0 0 24 24"
1043
+ className={className}
1044
+ aria-hidden="true"
1045
+ >
1046
+ <path
1047
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
1048
+ fill="#4285F4"
1049
+ />
1050
+ <path
1051
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
1052
+ fill="#34A853"
1053
+ />
1054
+ <path
1055
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
1056
+ fill="#FBBC05"
1057
+ />
1058
+ <path
1059
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
1060
+ fill="#EA4335"
1061
+ />
1062
+ </svg>
1063
+ )
1064
+ }