@nexttylabs/echo 0.2.0

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 (579) hide show
  1. package/.changeset/README.md +21 -0
  2. package/.changeset/config.json +11 -0
  3. package/.changeset/cozy-ghosts-care.md +5 -0
  4. package/.changeset/sharp-lines-stand.md +5 -0
  5. package/.changeset/sour-doodles-eat.md +5 -0
  6. package/.changeset/tender-moose-shop.md +5 -0
  7. package/.github/pull_request_template.md +13 -0
  8. package/.github/workflows/ci.yml +41 -0
  9. package/.github/workflows/publish.yml +44 -0
  10. package/.github/workflows/release.yml +73 -0
  11. package/AGENTS.md +92 -0
  12. package/CHANGELOG.md +13 -0
  13. package/Dockerfile +57 -0
  14. package/LICENSE +661 -0
  15. package/Makefile +77 -0
  16. package/README.md +198 -0
  17. package/app/(auth)/login/page.tsx +53 -0
  18. package/app/(auth)/register/page.tsx +48 -0
  19. package/app/(auth)/sign-in/page.tsx +22 -0
  20. package/app/(dashboard)/admin/feedback/[id]/edit/page.tsx +103 -0
  21. package/app/(dashboard)/admin/feedback/[id]/page.tsx +154 -0
  22. package/app/(dashboard)/admin/feedback/new/page.tsx +91 -0
  23. package/app/(dashboard)/admin/feedback/page.tsx +81 -0
  24. package/app/(dashboard)/admin/layout.tsx +48 -0
  25. package/app/(dashboard)/analytics/portal/page.tsx +30 -0
  26. package/app/(dashboard)/dashboard/page.tsx +133 -0
  27. package/app/(dashboard)/layout.tsx +69 -0
  28. package/app/(dashboard)/no-access/page.tsx +45 -0
  29. package/app/(dashboard)/settings/access/page.tsx +56 -0
  30. package/app/(dashboard)/settings/api-keys/page.tsx +55 -0
  31. package/app/(dashboard)/settings/appearance/page.tsx +40 -0
  32. package/app/(dashboard)/settings/branding/page.tsx +62 -0
  33. package/app/(dashboard)/settings/changelog/page.tsx +51 -0
  34. package/app/(dashboard)/settings/danger-zone/page.tsx +92 -0
  35. package/app/(dashboard)/settings/feedback/page.tsx +63 -0
  36. package/app/(dashboard)/settings/integrations/page.tsx +94 -0
  37. package/app/(dashboard)/settings/layout.tsx +43 -0
  38. package/app/(dashboard)/settings/modules/page.tsx +54 -0
  39. package/app/(dashboard)/settings/notifications/page.tsx +48 -0
  40. package/app/(dashboard)/settings/organization/page.tsx +104 -0
  41. package/app/(dashboard)/settings/organization/portal/access/page.tsx +22 -0
  42. package/app/(dashboard)/settings/organization/portal/experience/page.tsx +22 -0
  43. package/app/(dashboard)/settings/organization/portal/growth/page.tsx +22 -0
  44. package/app/(dashboard)/settings/organization/portal/layout.tsx +24 -0
  45. package/app/(dashboard)/settings/organization/portal/page.tsx +22 -0
  46. package/app/(dashboard)/settings/organizations/[orgId]/members/page.tsx +69 -0
  47. package/app/(dashboard)/settings/organizations/new/page.tsx +36 -0
  48. package/app/(dashboard)/settings/page.tsx +22 -0
  49. package/app/(dashboard)/settings/portal-access/page.tsx +53 -0
  50. package/app/(dashboard)/settings/portal-branding/page.tsx +59 -0
  51. package/app/(dashboard)/settings/portal-growth/page.tsx +57 -0
  52. package/app/(dashboard)/settings/portal-modules/page.tsx +49 -0
  53. package/app/(dashboard)/settings/portal-resources/page.tsx +66 -0
  54. package/app/(dashboard)/settings/profile/page.tsx +48 -0
  55. package/app/(dashboard)/settings/widgets/page.tsx +63 -0
  56. package/app/(public)/[organizationSlug]/changelog/page.tsx +109 -0
  57. package/app/(public)/[organizationSlug]/feedback/[id]/page.tsx +146 -0
  58. package/app/(public)/[organizationSlug]/page.tsx +160 -0
  59. package/app/(public)/[organizationSlug]/roadmap/page.tsx +142 -0
  60. package/app/(public)/docs/page.tsx +48 -0
  61. package/app/(public)/feedback/[id]/not-found.tsx +33 -0
  62. package/app/(public)/feedback/[id]/page.tsx +102 -0
  63. package/app/(public)/invite/[token]/page.tsx +121 -0
  64. package/app/(public)/page.tsx +22 -0
  65. package/app/(public)/widget/[organizationId]/page.tsx +122 -0
  66. package/app/api/_utils.ts +29 -0
  67. package/app/api/admin/backup/route.ts +72 -0
  68. package/app/api/api-keys/[keyId]/route.ts +92 -0
  69. package/app/api/api-keys/route.ts +116 -0
  70. package/app/api/auth/[...all]/route.ts +21 -0
  71. package/app/api/auth/clear-session/route.ts +43 -0
  72. package/app/api/auth/register/handler.ts +176 -0
  73. package/app/api/auth/register/route.ts +26 -0
  74. package/app/api/docs/route.ts +28 -0
  75. package/app/api/feedback/[id]/comments/[commentId]/route.ts +105 -0
  76. package/app/api/feedback/[id]/comments/route.ts +421 -0
  77. package/app/api/feedback/[id]/duplicates/route.ts +285 -0
  78. package/app/api/feedback/[id]/handler.ts +91 -0
  79. package/app/api/feedback/[id]/processing-status/route.ts +199 -0
  80. package/app/api/feedback/[id]/reclassify/route.ts +145 -0
  81. package/app/api/feedback/[id]/route.ts +511 -0
  82. package/app/api/feedback/[id]/suggest-tags/route.ts +227 -0
  83. package/app/api/feedback/[id]/sync-github/route.ts +52 -0
  84. package/app/api/feedback/[id]/vote/route.ts +431 -0
  85. package/app/api/feedback/bulk/route.ts +212 -0
  86. package/app/api/feedback/handler.ts +138 -0
  87. package/app/api/feedback/route.ts +298 -0
  88. package/app/api/feedback/similar/route.ts +100 -0
  89. package/app/api/health/route.test.ts +64 -0
  90. package/app/api/health/route.ts +92 -0
  91. package/app/api/identify/jwt/route.ts +29 -0
  92. package/app/api/integrations/github/route.ts +196 -0
  93. package/app/api/internal/domain-lookup/route.ts +67 -0
  94. package/app/api/invitations/accept/handler.ts +101 -0
  95. package/app/api/invitations/accept/route.ts +29 -0
  96. package/app/api/notifications/preferences/route.ts +109 -0
  97. package/app/api/organizations/[orgId]/handler.ts +123 -0
  98. package/app/api/organizations/[orgId]/invitations/handler.ts +121 -0
  99. package/app/api/organizations/[orgId]/invitations/route.ts +29 -0
  100. package/app/api/organizations/[orgId]/members/[memberId]/handler.ts +208 -0
  101. package/app/api/organizations/[orgId]/members/[memberId]/route.ts +30 -0
  102. package/app/api/organizations/[orgId]/members/handler.ts +77 -0
  103. package/app/api/organizations/[orgId]/members/route.ts +29 -0
  104. package/app/api/organizations/[orgId]/route.ts +30 -0
  105. package/app/api/organizations/handler.ts +97 -0
  106. package/app/api/organizations/route.ts +29 -0
  107. package/app/api/tags/sync/route.ts +88 -0
  108. package/app/api/upload/handler.ts +79 -0
  109. package/app/api/upload/route.ts +37 -0
  110. package/app/api/v1/feedback/[id]/route.ts +276 -0
  111. package/app/api/v1/feedback/route.ts +250 -0
  112. package/app/api/v1/spec/route.ts +356 -0
  113. package/app/api/webhooks/[webhookId]/route.ts +213 -0
  114. package/app/api/webhooks/github/route.ts +158 -0
  115. package/app/api/webhooks/route.ts +143 -0
  116. package/app/favicon.ico +0 -0
  117. package/app/globals.css +139 -0
  118. package/app/health/route.ts +108 -0
  119. package/app/layout.tsx +60 -0
  120. package/bun.lock +2503 -0
  121. package/components/api/rate-limit-info.tsx +86 -0
  122. package/components/api-keys/api-key-manager.tsx +262 -0
  123. package/components/auth/login-form.tsx +207 -0
  124. package/components/auth/register-form.tsx +230 -0
  125. package/components/comment/comment-form.tsx +111 -0
  126. package/components/comment/internal-notes.tsx +219 -0
  127. package/components/comment/public-comments.tsx +387 -0
  128. package/components/component-example-client-only.tsx +29 -0
  129. package/components/component-example.tsx +519 -0
  130. package/components/dashboard/index.ts +22 -0
  131. package/components/dashboard/organization-switcher.tsx +96 -0
  132. package/components/dashboard/quick-actions.tsx +57 -0
  133. package/components/dashboard/recent-feedback-list.tsx +152 -0
  134. package/components/dashboard/stats-cards.tsx +88 -0
  135. package/components/dashboard/status-chart.tsx +106 -0
  136. package/components/example.tsx +70 -0
  137. package/components/feedback/attachment-list.tsx +103 -0
  138. package/components/feedback/auto-classification-badge.tsx +92 -0
  139. package/components/feedback/classification-override.tsx +64 -0
  140. package/components/feedback/duplicate-suggestions-inline.tsx +158 -0
  141. package/components/feedback/duplicate-suggestions.tsx +188 -0
  142. package/components/feedback/embedded-feedback-form.tsx +439 -0
  143. package/components/feedback/feedback-actions.tsx +160 -0
  144. package/components/feedback/feedback-bulk-actions.tsx +184 -0
  145. package/components/feedback/feedback-detail-view.tsx +321 -0
  146. package/components/feedback/feedback-detail.tsx +305 -0
  147. package/components/feedback/feedback-edit-form.tsx +131 -0
  148. package/components/feedback/feedback-filters.tsx +222 -0
  149. package/components/feedback/feedback-list-controls.tsx +433 -0
  150. package/components/feedback/feedback-list-item.tsx +298 -0
  151. package/components/feedback/feedback-list-skeleton.tsx +49 -0
  152. package/components/feedback/feedback-list.tsx +523 -0
  153. package/components/feedback/feedback-sorter.tsx +117 -0
  154. package/components/feedback/feedback-stats.tsx +124 -0
  155. package/components/feedback/file-upload.tsx +289 -0
  156. package/components/feedback/processing-status.tsx +161 -0
  157. package/components/feedback/status-history.tsx +134 -0
  158. package/components/feedback/status-selector.tsx +153 -0
  159. package/components/feedback/submit-on-behalf-form.tsx +403 -0
  160. package/components/feedback/tag-suggestions.tsx +212 -0
  161. package/components/feedback/vote-button.tsx +113 -0
  162. package/components/feedback/vote-list.tsx +108 -0
  163. package/components/integrations/github-config.tsx +200 -0
  164. package/components/landing/hero.tsx +150 -0
  165. package/components/layout/dashboard-layout.tsx +59 -0
  166. package/components/layout/index.ts +20 -0
  167. package/components/layout/language-switcher.tsx +129 -0
  168. package/components/layout/mobile-sidebar.tsx +66 -0
  169. package/components/layout/sidebar.tsx +279 -0
  170. package/components/portal/changelog-entry.tsx +132 -0
  171. package/components/portal/changelog-list.tsx +85 -0
  172. package/components/portal/contributor-badge.tsx +29 -0
  173. package/components/portal/contributors-sidebar.tsx +98 -0
  174. package/components/portal/create-post-dialog.tsx +247 -0
  175. package/components/portal/feedback-board.tsx +205 -0
  176. package/components/portal/feedback-post-card.tsx +198 -0
  177. package/components/portal/help-center.tsx +169 -0
  178. package/components/portal/leaderboard.tsx +29 -0
  179. package/components/portal/portal-header.tsx +153 -0
  180. package/components/portal/portal-layout.tsx +62 -0
  181. package/components/portal/portal-modules-panel.tsx +118 -0
  182. package/components/portal/portal-nav.tsx +59 -0
  183. package/components/portal/portal-overview.tsx +174 -0
  184. package/components/portal/portal-settings-nav.tsx +62 -0
  185. package/components/portal/portal-settings-shell.tsx +71 -0
  186. package/components/portal/portal-shell.tsx +62 -0
  187. package/components/portal/portal-tab-nav.tsx +77 -0
  188. package/components/portal/project-switcher.tsx +20 -0
  189. package/components/portal/roadmap-board.tsx +82 -0
  190. package/components/portal/roadmap-card.tsx +76 -0
  191. package/components/portal/roadmap-column.tsx +78 -0
  192. package/components/portal/settings-forms/access-form.tsx +194 -0
  193. package/components/portal/settings-forms/copy-form.tsx +95 -0
  194. package/components/portal/settings-forms/index.ts +23 -0
  195. package/components/portal/settings-forms/languages-form.tsx +223 -0
  196. package/components/portal/settings-forms/seo-form.tsx +156 -0
  197. package/components/portal/settings-forms/sharing-form.tsx +155 -0
  198. package/components/portal/settings-forms/theme-form.tsx +104 -0
  199. package/components/settings/api-keys-list.tsx +167 -0
  200. package/components/settings/appearance-form.tsx +71 -0
  201. package/components/settings/index.ts +25 -0
  202. package/components/settings/invite-member-form.tsx +119 -0
  203. package/components/settings/notification-preferences.tsx +174 -0
  204. package/components/settings/organization-form.tsx +165 -0
  205. package/components/settings/organization-members-list.tsx +197 -0
  206. package/components/settings/profile-form.tsx +124 -0
  207. package/components/settings/role-selector.tsx +57 -0
  208. package/components/settings/settings-sidebar.tsx +115 -0
  209. package/components/shared/pagination.tsx +215 -0
  210. package/components/ui/alert-dialog.tsx +201 -0
  211. package/components/ui/alert.tsx +75 -0
  212. package/components/ui/avatar.tsx +126 -0
  213. package/components/ui/badge.tsx +62 -0
  214. package/components/ui/button.tsx +77 -0
  215. package/components/ui/card.tsx +111 -0
  216. package/components/ui/combobox.tsx +311 -0
  217. package/components/ui/dialog.tsx +158 -0
  218. package/components/ui/dropdown-menu.tsx +272 -0
  219. package/components/ui/field.tsx +256 -0
  220. package/components/ui/input-group.tsx +164 -0
  221. package/components/ui/input.tsx +36 -0
  222. package/components/ui/label.tsx +41 -0
  223. package/components/ui/pagination.tsx +142 -0
  224. package/components/ui/select.tsx +202 -0
  225. package/components/ui/separator.tsx +45 -0
  226. package/components/ui/sheet.tsx +151 -0
  227. package/components/ui/skeleton.tsx +32 -0
  228. package/components/ui/switch.tsx +49 -0
  229. package/components/ui/table.tsx +118 -0
  230. package/components/ui/tabs.tsx +107 -0
  231. package/components/ui/textarea.tsx +35 -0
  232. package/components/ui/tooltip.tsx +78 -0
  233. package/components/widget/widget-form.tsx +439 -0
  234. package/components.json +24 -0
  235. package/db/init/01-init.sql +13 -0
  236. package/docker-compose.dev.yml +26 -0
  237. package/docker-compose.yml +98 -0
  238. package/docs/architecture.md +259 -0
  239. package/docs/component-inventory.md +261 -0
  240. package/docs/database-migrations.md +76 -0
  241. package/docs/development-guide.md +209 -0
  242. package/docs/e2e-user-flows.csv +31 -0
  243. package/docs/er-diagram-feedback.mmd +138 -0
  244. package/docs/er-diagram.mmd +281 -0
  245. package/docs/i18n-check-report.md +296 -0
  246. package/docs/index.md +214 -0
  247. package/docs/logic-chain.md +94 -0
  248. package/docs/plans/2026-01-02-database-migration-scripts.md +496 -0
  249. package/docs/plans/2026-01-02-user-login-design.md +37 -0
  250. package/docs/plans/2026-01-02-user-login.md +437 -0
  251. package/docs/plans/2026-01-02-user-registration-design.md +47 -0
  252. package/docs/plans/2026-01-02-user-registration.md +628 -0
  253. package/docs/plans/2026-01-03-roles-permissions-design.md +20 -0
  254. package/docs/plans/2026-01-03-roles-permissions.md +266 -0
  255. package/docs/plans/2026-01-05-authentication-middleware.md +207 -0
  256. package/docs/plans/2026-01-05-member-removal.md +186 -0
  257. package/docs/plans/2026-01-05-organization-creation.md +374 -0
  258. package/docs/plans/2026-01-05-rbac-middleware.md +112 -0
  259. package/docs/plans/2026-01-05-role-configuration.md +441 -0
  260. package/docs/plans/2026-01-06-file-upload-support.md +804 -0
  261. package/docs/plans/2026-01-06-permission-check-hook.md +155 -0
  262. package/docs/plans/2026-01-06-resource-ownership-check.md +231 -0
  263. package/docs/plans/2026-01-07-feedback-tracking-link.md +459 -0
  264. package/docs/plans/2026-01-09-logout-redirect-design.md +52 -0
  265. package/docs/plans/2026-01-09-phase2-3-plan.md +654 -0
  266. package/docs/plans/2026-01-09-portal-execution-plan.md +408 -0
  267. package/docs/plans/2026-01-09-project-delete-feature-design.md +163 -0
  268. package/docs/plans/2026-01-09-project-delete-implementation.md +451 -0
  269. package/docs/plans/2026-01-09-project-edit-delete-design.md +52 -0
  270. package/docs/plans/2026-01-09-settings-center-design.md +114 -0
  271. package/docs/plans/2026-01-09-settings-center.md +948 -0
  272. package/docs/plans/2026-01-10-organization-only-design.md +66 -0
  273. package/docs/plans/2026-01-10-organization-only-implementation.md +433 -0
  274. package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +18 -0
  275. package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +296 -0
  276. package/docs/plans/2026-01-14-e2e-playwright-feedback.md +173 -0
  277. package/docs/plans/2026-01-15-feedback-management-org-context-design.md +82 -0
  278. package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +521 -0
  279. package/docs/plans/2026-01-16-admin-feedback-filters-design.md +75 -0
  280. package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +293 -0
  281. package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +180 -0
  282. package/docs/plans/2026-01-16-e2e-test-fixes.md +158 -0
  283. package/docs/plans/2026-01-17-admin-feedback-filters.md +214 -0
  284. package/docs/plans/2026-01-17-admin-feedback-improvements.md +453 -0
  285. package/docs/plans/2026-01-18-changesets-design.md +40 -0
  286. package/docs/product_changes.md +37 -0
  287. package/docs/project-overview.md +159 -0
  288. package/docs/project-scan-report.json +104 -0
  289. package/docs/route-role-visibility.md +51 -0
  290. package/docs/source-tree-analysis.md +150 -0
  291. package/docs/testing/delete-project-manual-tests.md +18 -0
  292. package/docs/user-story-tracking.md +191 -0
  293. package/drizzle.config.ts +32 -0
  294. package/eslint.config.mjs +19 -0
  295. package/hooks/use-permissions.ts +56 -0
  296. package/i18n/config.ts +45 -0
  297. package/i18n/request.ts +28 -0
  298. package/i18n/resolve-locale.ts +38 -0
  299. package/lib/api/errors.ts +62 -0
  300. package/lib/auth/cli-config.ts +35 -0
  301. package/lib/auth/client.ts +20 -0
  302. package/lib/auth/config.ts +55 -0
  303. package/lib/auth/jwt-identity.ts +21 -0
  304. package/lib/auth/org-context.ts +71 -0
  305. package/lib/auth/organization.ts +107 -0
  306. package/lib/auth/permissions.ts +87 -0
  307. package/lib/auth/session.ts +23 -0
  308. package/lib/config/rate-limits.ts +64 -0
  309. package/lib/dashboard/get-dashboard-stats.ts +136 -0
  310. package/lib/db/index.ts +41 -0
  311. package/lib/db/migrate.test.ts +49 -0
  312. package/lib/db/migrate.ts +62 -0
  313. package/lib/db/migrations/.gitkeep +0 -0
  314. package/lib/db/migrations/0000_cynical_gladiator.sql +53 -0
  315. package/lib/db/migrations/0001_wandering_sunfire.sql +27 -0
  316. package/lib/db/migrations/0002_shallow_speedball.sql +1 -0
  317. package/lib/db/migrations/0003_add_org_description.sql +1 -0
  318. package/lib/db/migrations/0003_boring_wild_pack.sql +13 -0
  319. package/lib/db/migrations/0004_windy_tyrannus.sql +27 -0
  320. package/lib/db/migrations/0005_perpetual_doorman.sql +5 -0
  321. package/lib/db/migrations/0006_aberrant_captain_midlands.sql +13 -0
  322. package/lib/db/migrations/0007_clever_captain_cross.sql +14 -0
  323. package/lib/db/migrations/0008_sparkling_pandemic.sql +2 -0
  324. package/lib/db/migrations/0009_happy_black_tom.sql +29 -0
  325. package/lib/db/migrations/0010_kind_junta.sql +8 -0
  326. package/lib/db/migrations/0011_mute_squadron_supreme.sql +25 -0
  327. package/lib/db/migrations/0012_giant_power_man.sql +24 -0
  328. package/lib/db/migrations/0013_damp_titanium_man.sql +17 -0
  329. package/lib/db/migrations/0014_blue_alice.sql +18 -0
  330. package/lib/db/migrations/0015_webhook_tables.sql +41 -0
  331. package/lib/db/migrations/0016_github_integration.sql +30 -0
  332. package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +22 -0
  333. package/lib/db/migrations/0017_slimy_inhumans.sql +6 -0
  334. package/lib/db/migrations/0018_same_spitfire.sql +1 -0
  335. package/lib/db/migrations/0019_jittery_loners.sql +16 -0
  336. package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +14 -0
  337. package/lib/db/migrations/meta/0000_snapshot.json +374 -0
  338. package/lib/db/migrations/meta/0001_snapshot.json +553 -0
  339. package/lib/db/migrations/meta/0002_snapshot.json +560 -0
  340. package/lib/db/migrations/meta/0003_snapshot.json +650 -0
  341. package/lib/db/migrations/meta/0004_snapshot.json +852 -0
  342. package/lib/db/migrations/meta/0005_snapshot.json +900 -0
  343. package/lib/db/migrations/meta/0006_snapshot.json +1011 -0
  344. package/lib/db/migrations/meta/0007_snapshot.json +1125 -0
  345. package/lib/db/migrations/meta/0008_snapshot.json +1146 -0
  346. package/lib/db/migrations/meta/0009_snapshot.json +1386 -0
  347. package/lib/db/migrations/meta/0010_snapshot.json +1419 -0
  348. package/lib/db/migrations/meta/0011_snapshot.json +1615 -0
  349. package/lib/db/migrations/meta/0012_snapshot.json +1805 -0
  350. package/lib/db/migrations/meta/0013_snapshot.json +1948 -0
  351. package/lib/db/migrations/meta/0014_snapshot.json +2082 -0
  352. package/lib/db/migrations/meta/0015_snapshot.json +2476 -0
  353. package/lib/db/migrations/meta/0016_snapshot.json +2633 -0
  354. package/lib/db/migrations/meta/0017_snapshot.json +2680 -0
  355. package/lib/db/migrations/meta/0018_snapshot.json +2686 -0
  356. package/lib/db/migrations/meta/0019_snapshot.json +2741 -0
  357. package/lib/db/migrations/meta/_journal.json +146 -0
  358. package/lib/db/schema/ai-processing.ts +90 -0
  359. package/lib/db/schema/api-keys.ts +61 -0
  360. package/lib/db/schema/attachments.ts +48 -0
  361. package/lib/db/schema/auth.ts +111 -0
  362. package/lib/db/schema/comments.ts +74 -0
  363. package/lib/db/schema/duplicates.ts +80 -0
  364. package/lib/db/schema/feedback.ts +88 -0
  365. package/lib/db/schema/github-integrations.ts +66 -0
  366. package/lib/db/schema/index.ts +35 -0
  367. package/lib/db/schema/invitations.ts +32 -0
  368. package/lib/db/schema/notifications.ts +85 -0
  369. package/lib/db/schema/organization-members.ts +37 -0
  370. package/lib/db/schema/organization-settings.ts +134 -0
  371. package/lib/db/schema/organizations.ts +30 -0
  372. package/lib/db/schema/projects.ts +145 -0
  373. package/lib/db/schema/status-history.ts +63 -0
  374. package/lib/db/schema/tags.ts +194 -0
  375. package/lib/db/schema/user-profiles.ts +31 -0
  376. package/lib/db/schema/votes.ts +60 -0
  377. package/lib/db/schema/webhooks.ts +106 -0
  378. package/lib/feedback/filters.ts +28 -0
  379. package/lib/feedback/find-similar.ts +49 -0
  380. package/lib/feedback/get-feedback-by-id.ts +159 -0
  381. package/lib/feedback/prefill.ts +51 -0
  382. package/lib/http/get-request-url.ts +28 -0
  383. package/lib/integrations/github.ts +159 -0
  384. package/lib/invitations.ts +22 -0
  385. package/lib/logger.test.ts +31 -0
  386. package/lib/logger.ts +58 -0
  387. package/lib/middleware/api-key.ts +126 -0
  388. package/lib/middleware/rate-limit-keys.ts +47 -0
  389. package/lib/middleware/rate-limit.ts +148 -0
  390. package/lib/middleware/rbac.ts +39 -0
  391. package/lib/middleware/request-id.test.ts +28 -0
  392. package/lib/middleware/request-id.ts +30 -0
  393. package/lib/middleware/request-logger.test.ts +36 -0
  394. package/lib/middleware/request-logger.ts +41 -0
  395. package/lib/middleware/with-rate-limit.ts +33 -0
  396. package/lib/portal/analytics.ts +20 -0
  397. package/lib/portal/contributors.ts +27 -0
  398. package/lib/portal/i18n.ts +20 -0
  399. package/lib/portal/leaderboard-settings.ts +20 -0
  400. package/lib/portal/modules.ts +20 -0
  401. package/lib/portal/portal-copy.ts +20 -0
  402. package/lib/portal/public-context.tsx +110 -0
  403. package/lib/portal/seo.ts +20 -0
  404. package/lib/portal/settings-context.ts +56 -0
  405. package/lib/portal/sharing.ts +20 -0
  406. package/lib/portal/sorting.ts +20 -0
  407. package/lib/portal/theme.ts +20 -0
  408. package/lib/services/ai/classifier.ts +296 -0
  409. package/lib/services/ai/duplicate-detector.ts +255 -0
  410. package/lib/services/ai/tag-suggester.ts +108 -0
  411. package/lib/services/api-keys.ts +164 -0
  412. package/lib/services/backup.ts +173 -0
  413. package/lib/services/email/templates.ts +158 -0
  414. package/lib/services/email.ts +68 -0
  415. package/lib/services/github-sync.ts +205 -0
  416. package/lib/services/notifications/index.ts +224 -0
  417. package/lib/services/portal-settings.ts +157 -0
  418. package/lib/swagger/config.ts +296 -0
  419. package/lib/swagger/generate.ts +400 -0
  420. package/lib/upload/file-validator.ts +52 -0
  421. package/lib/upload/storage.ts +59 -0
  422. package/lib/utils/format.ts +26 -0
  423. package/lib/utils/slug.ts +28 -0
  424. package/lib/utils.ts +23 -0
  425. package/lib/validations/auth.ts +56 -0
  426. package/lib/validations/comment.ts +44 -0
  427. package/lib/validations/feedback.ts +51 -0
  428. package/lib/validations/invitations.ts +23 -0
  429. package/lib/validations/organizations.ts +34 -0
  430. package/lib/validations/projects.ts +49 -0
  431. package/lib/validators/feedback.ts +57 -0
  432. package/lib/validators/index.ts +18 -0
  433. package/lib/webhooks/events.ts +73 -0
  434. package/lib/webhooks/index.ts +21 -0
  435. package/lib/webhooks/retry.ts +188 -0
  436. package/lib/webhooks/sender.ts +183 -0
  437. package/lib/webhooks/verify.ts +37 -0
  438. package/lib/workers/feedback-processor.ts +255 -0
  439. package/messages/en.json +965 -0
  440. package/messages/jp.json +862 -0
  441. package/messages/zh-CN.json +855 -0
  442. package/next-env.d.ts +6 -0
  443. package/next.config.ts +66 -0
  444. package/package.json +84 -0
  445. package/playwright.config.ts +44 -0
  446. package/postcss.config.mjs +7 -0
  447. package/proxy.test.ts +131 -0
  448. package/proxy.ts +190 -0
  449. package/public/file.svg +1 -0
  450. package/public/globe.svg +1 -0
  451. package/public/logo-64.svg +5 -0
  452. package/public/logo.svg +5 -0
  453. package/public/next.svg +1 -0
  454. package/public/openapi.json +673 -0
  455. package/public/uploads/.gitkeep +0 -0
  456. package/public/uploads/02695701-ded0-4c81-8a21-9326c1d65448.pdf +1 -0
  457. package/public/uploads/178843ea-2780-48ef-8988-f4cba442e4cb.pdf +1 -0
  458. package/public/uploads/24b0a9ef-da93-49da-934f-637f89c7871d.pdf +1 -0
  459. package/public/uploads/7a11626d-a8e4-4b91-a8eb-20b6213b0a5a.pdf +1 -0
  460. package/public/uploads/b0703f4d-6e7b-4aab-8191-1a7b15f1b8ee.pdf +1 -0
  461. package/public/uploads/c8de0aed-4d3a-44aa-83bb-6594b7a2ddb3.pdf +1 -0
  462. package/public/uploads/e4cce295-0d85-4525-a1b0-a61c45722e26.pdf +1 -0
  463. package/public/uploads/eb4df45e-563c-48b8-9c68-c18212312426.pdf +1 -0
  464. package/public/vercel.svg +1 -0
  465. package/public/widget/embed.js +249 -0
  466. package/public/window.svg +1 -0
  467. package/scripts/backup-db.sh +57 -0
  468. package/scripts/backup-db.ts +24 -0
  469. package/scripts/generate-openapi.ts +22 -0
  470. package/scripts/migration-helper.ts +39 -0
  471. package/scripts/pre-deploy.ts +75 -0
  472. package/scripts/restore-db.sh +60 -0
  473. package/scripts/rollback.ts +72 -0
  474. package/scripts/seed-tags.ts +48 -0
  475. package/tests/api/feedback-bulk.test.ts +47 -0
  476. package/tests/api/feedback-by-id.test.ts +67 -0
  477. package/tests/api/feedback-comments-route-import.test.ts +26 -0
  478. package/tests/api/feedback-create.test.ts +71 -0
  479. package/tests/api/feedback-delete.test.ts +160 -0
  480. package/tests/api/feedback-filter.test.ts +250 -0
  481. package/tests/api/feedback-list.test.ts +234 -0
  482. package/tests/api/feedback-route-assignee-condition.test.ts +32 -0
  483. package/tests/api/feedback-similar.test.ts +46 -0
  484. package/tests/api/feedback-sort.test.ts +261 -0
  485. package/tests/api/feedback-status-enum.test.ts +49 -0
  486. package/tests/api/feedback-status-filter.test.ts +117 -0
  487. package/tests/api/feedback-submit-on-behalf.test.ts +269 -0
  488. package/tests/api/feedback.test.ts +175 -0
  489. package/tests/api/identify-jwt.test.ts +25 -0
  490. package/tests/api/invitation-accept.test.ts +213 -0
  491. package/tests/api/organization-invitations.test.ts +186 -0
  492. package/tests/api/organization-members-list.test.ts +79 -0
  493. package/tests/api/organization-members.test.ts +340 -0
  494. package/tests/api/organizations.test.ts +149 -0
  495. package/tests/api/register.test.ts +112 -0
  496. package/tests/api/upload.test.ts +103 -0
  497. package/tests/api/vote.test.ts +82 -0
  498. package/tests/app/admin-feedback-detail-page.test.tsx +25 -0
  499. package/tests/app/admin-feedback-list-page.test.tsx +25 -0
  500. package/tests/app/admin-feedback-new-page.test.tsx +25 -0
  501. package/tests/app/health-route-helpers.test.ts +27 -0
  502. package/tests/app/login-page.test.ts +26 -0
  503. package/tests/app/portal-page.test.ts +29 -0
  504. package/tests/app/project-portal-overview.test.tsx +25 -0
  505. package/tests/app/widget-page-import.test.ts +25 -0
  506. package/tests/components/create-post-dialog-defaults.test.ts +43 -0
  507. package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +27 -0
  508. package/tests/components/feedback/embedded-feedback-form.test.tsx +96 -0
  509. package/tests/components/feedback/feedback-detail.test.tsx +25 -0
  510. package/tests/components/feedback/feedback-stats.test.tsx +49 -0
  511. package/tests/components/feedback-bulk-actions.test.tsx +39 -0
  512. package/tests/components/feedback-i18n-keys.test.ts +70 -0
  513. package/tests/components/feedback-list-controls-compile.test.ts +25 -0
  514. package/tests/components/feedback-list-controls.test.tsx +204 -0
  515. package/tests/components/feedback-list-item.test.tsx +67 -0
  516. package/tests/components/landing/hero.test.tsx +46 -0
  517. package/tests/components/layout/language-switcher.test.tsx +25 -0
  518. package/tests/components/layout/sidebar.test.tsx +157 -0
  519. package/tests/components/login-form.test.ts +25 -0
  520. package/tests/components/organization-form.test.ts +32 -0
  521. package/tests/components/organization-switcher.test.ts +25 -0
  522. package/tests/components/pagination.test.tsx +43 -0
  523. package/tests/components/portal-overview.test.tsx +25 -0
  524. package/tests/components/profile-form.test.tsx +139 -0
  525. package/tests/components/role-selector.test.ts +31 -0
  526. package/tests/components/status-chart.test.tsx +90 -0
  527. package/tests/e2e/auth.e2e.ts +323 -0
  528. package/tests/e2e/feedback-actions.e2e.ts +471 -0
  529. package/tests/e2e/feedback-attachment.e2e.ts +168 -0
  530. package/tests/e2e/feedback-customer.e2e.ts +226 -0
  531. package/tests/e2e/feedback-management.e2e.ts +565 -0
  532. package/tests/e2e/feedback-submit.e2e.ts +133 -0
  533. package/tests/e2e/feedback-view.e2e.ts +297 -0
  534. package/tests/e2e/fixtures/test-data.ts +235 -0
  535. package/tests/e2e/health-check.e2e.ts +230 -0
  536. package/tests/e2e/helpers/test-utils-helpers.test.ts +43 -0
  537. package/tests/e2e/helpers/test-utils.ts +298 -0
  538. package/tests/e2e/integration-placeholders.e2e.ts +199 -0
  539. package/tests/e2e/organization.e2e.ts +292 -0
  540. package/tests/e2e/permissions.e2e.ts +424 -0
  541. package/tests/e2e/project-widget.e2e.ts +63 -0
  542. package/tests/feedback/filters.test.ts +29 -0
  543. package/tests/hooks/use-permissions.test.ts +52 -0
  544. package/tests/lib/ai/classifier.test.ts +104 -0
  545. package/tests/lib/ai/duplicate-detector.test.ts +234 -0
  546. package/tests/lib/attachments-schema.test.ts +30 -0
  547. package/tests/lib/auth/session.test.ts +49 -0
  548. package/tests/lib/auth-client.test.ts +37 -0
  549. package/tests/lib/auth-config.test.ts +26 -0
  550. package/tests/lib/feedback-prefill.test.ts +52 -0
  551. package/tests/lib/feedback-processor.test.ts +41 -0
  552. package/tests/lib/feedback-schema.test.ts +33 -0
  553. package/tests/lib/file-validator.test.ts +48 -0
  554. package/tests/lib/get-feedback-by-id.test.ts +37 -0
  555. package/tests/lib/invitations.test.ts +35 -0
  556. package/tests/lib/login-schema.test.ts +36 -0
  557. package/tests/lib/org-context.test.ts +95 -0
  558. package/tests/lib/organization-access.test.ts +44 -0
  559. package/tests/lib/organization-member-role-schema.test.ts +41 -0
  560. package/tests/lib/permissions.test.ts +88 -0
  561. package/tests/lib/portal-analytics.test.ts +25 -0
  562. package/tests/lib/portal-contributors.test.ts +25 -0
  563. package/tests/lib/portal-copy.test.ts +27 -0
  564. package/tests/lib/portal-i18n.test.ts +30 -0
  565. package/tests/lib/portal-leaderboard-settings.test.ts +25 -0
  566. package/tests/lib/portal-modules.test.ts +25 -0
  567. package/tests/lib/portal-seo.test.ts +25 -0
  568. package/tests/lib/portal-sharing.test.ts +25 -0
  569. package/tests/lib/portal-sorting.test.ts +25 -0
  570. package/tests/lib/portal-theme.test.ts +25 -0
  571. package/tests/lib/rate-limit.test.ts +142 -0
  572. package/tests/lib/resolve-locale.test.ts +34 -0
  573. package/tests/lib/services/backup.test.ts +145 -0
  574. package/tests/lib/user-organizations.test.ts +42 -0
  575. package/tests/lib/user-role-schema.test.ts +33 -0
  576. package/tests/lib/user-schema.test.ts +25 -0
  577. package/tests/setup.ts +74 -0
  578. package/tsconfig.json +34 -0
  579. package/types/bun-test.d.ts +31 -0
@@ -0,0 +1,628 @@
1
+ # User Registration Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Implement user registration with Better Auth (email/password), create a profile + default organization, and provide a register UI.
6
+
7
+ **Architecture:** Better Auth owns auth tables and session cookies. Business data (profiles/organizations/members) lives in our own tables linked by userId. A custom `/api/auth/register` route validates input, calls `auth.api.signUpEmail`, then creates profile/org/member records.
8
+
9
+ **Tech Stack:** Next.js App Router, Better Auth, Drizzle ORM, Bun, Zod, Tailwind, shadcn/ui.
10
+
11
+ **Skills:** @superpowers:test-driven-development, @senior-frontend, @ui-styling
12
+
13
+ ---
14
+
15
+ ### Task 1: Bootstrap Better Auth + schema generation
16
+
17
+ **Files:**
18
+ - Modify: `package.json`
19
+ - Create: `lib/auth/config.ts`
20
+ - Create: `app/api/auth/[...all]/route.ts`
21
+ - Create (generated): `lib/db/schema/auth.ts`
22
+ - Modify: `lib/db/schema/index.ts`
23
+
24
+ **Step 1: Add dependencies**
25
+
26
+ Run:
27
+ ```bash
28
+ bun add better-auth zod
29
+ ```
30
+ Expected: deps added in `package.json` and lockfile.
31
+
32
+ **Step 2: Create Better Auth config**
33
+
34
+ Create `lib/auth/config.ts` (use relative imports so the Better Auth CLI can resolve modules):
35
+ ```ts
36
+ import { betterAuth } from "better-auth";
37
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
38
+ import { nextCookies } from "better-auth/next-js";
39
+ import { db } from "../db";
40
+
41
+ if (!db) {
42
+ throw new Error("DATABASE_URL is not configured");
43
+ }
44
+
45
+ export const auth = betterAuth({
46
+ database: drizzleAdapter(db, {
47
+ provider: "pg",
48
+ }),
49
+ emailAndPassword: {
50
+ enabled: true,
51
+ },
52
+ plugins: [nextCookies()], // keep this last
53
+ });
54
+ ```
55
+
56
+ **Step 3: Mount Better Auth handler**
57
+
58
+ Create `app/api/auth/[...all]/route.ts`:
59
+ ```ts
60
+ import { auth } from "@/lib/auth/config";
61
+ import { toNextJsHandler } from "better-auth/next-js";
62
+
63
+ export const { GET, POST } = toNextJsHandler(auth);
64
+ ```
65
+
66
+ **Step 4: Generate Better Auth Drizzle schema**
67
+
68
+ Run:
69
+ ```bash
70
+ bunx @better-auth/cli@latest generate --config lib/auth/config.ts --output lib/db/schema/auth.ts
71
+ ```
72
+ Expected: a new Drizzle schema file defining Better Auth tables.
73
+
74
+ **Step 5: Export schema for drizzle-kit**
75
+
76
+ Update `lib/db/schema/index.ts`:
77
+ ```ts
78
+ export * from "./auth";
79
+ ```
80
+
81
+ **Step 6: Generate migration (no apply yet)**
82
+
83
+ Run:
84
+ ```bash
85
+ bun run db:generate
86
+ ```
87
+ Expected: new SQL in `lib/db/migrations/`.
88
+
89
+ **Step 7: Commit**
90
+
91
+ ```bash
92
+ git add package.json bun.lockb lib/auth/config.ts app/api/auth/[...all]/route.ts lib/db/schema/auth.ts lib/db/schema/index.ts lib/db/migrations
93
+ git commit -m "feat: add better-auth config and schema"
94
+ ```
95
+
96
+ ---
97
+
98
+ ### Task 2: Business domain schemas + slug helper
99
+
100
+ **Files:**
101
+ - Create: `lib/db/schema/user-profiles.ts`
102
+ - Create: `lib/db/schema/organizations.ts`
103
+ - Create: `lib/db/schema/organization-members.ts`
104
+ - Modify: `lib/db/schema/index.ts`
105
+ - Create: `lib/utils/slug.ts`
106
+
107
+ **Step 1: Add schemas**
108
+
109
+ Create `lib/db/schema/user-profiles.ts` (adjust `user` import based on generated auth schema name):
110
+ ```ts
111
+ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
112
+ import { user } from "./auth"; // update if generated export name differs
113
+
114
+ export const userProfiles = pgTable("user_profiles", {
115
+ userId: text("user_id").primaryKey().references(() => user.id, { onDelete: "cascade" }),
116
+ name: text("name").notNull(),
117
+ createdAt: timestamp("created_at").defaultNow().notNull(),
118
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
119
+ });
120
+ ```
121
+
122
+ Create `lib/db/schema/organizations.ts`:
123
+ ```ts
124
+ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
125
+
126
+ export const organizations = pgTable("organizations", {
127
+ id: text("id").primaryKey(),
128
+ name: text("name").notNull(),
129
+ slug: text("slug").notNull().unique(),
130
+ createdAt: timestamp("created_at").defaultNow().notNull(),
131
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
132
+ });
133
+ ```
134
+
135
+ Create `lib/db/schema/organization-members.ts`:
136
+ ```ts
137
+ import { pgTable, primaryKey, text, timestamp } from "drizzle-orm/pg-core";
138
+ import { organizations } from "./organizations";
139
+ import { user } from "./auth"; // update if generated export name differs
140
+
141
+ export const organizationMembers = pgTable(
142
+ "organization_members",
143
+ {
144
+ organizationId: text("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
145
+ userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
146
+ role: text("role").notNull(),
147
+ createdAt: timestamp("created_at").defaultNow().notNull(),
148
+ },
149
+ (t) => ({
150
+ pk: primaryKey({ columns: [t.organizationId, t.userId] }),
151
+ })
152
+ );
153
+ ```
154
+
155
+ Update `lib/db/schema/index.ts`:
156
+ ```ts
157
+ export * from "./auth";
158
+ export * from "./user-profiles";
159
+ export * from "./organizations";
160
+ export * from "./organization-members";
161
+ ```
162
+
163
+ Create `lib/utils/slug.ts`:
164
+ ```ts
165
+ export function generateSlug(name: string): string {
166
+ const base = name
167
+ .toLowerCase()
168
+ .trim()
169
+ .replace(/\s+/g, "-")
170
+ .replace(/[^\w-]/g, "")
171
+ .replace(/--+/g, "-");
172
+
173
+ const suffix = Math.random().toString(36).slice(2, 6);
174
+ return `${base}-${suffix}`;
175
+ }
176
+ ```
177
+
178
+ **Step 2: Generate migration**
179
+
180
+ Run:
181
+ ```bash
182
+ bun run db:generate
183
+ ```
184
+ Expected: migration adds business tables.
185
+
186
+ **Step 3: Commit**
187
+
188
+ ```bash
189
+ git add lib/db/schema lib/utils/slug.ts lib/db/migrations
190
+ git commit -m "feat: add profile and organization schemas"
191
+ ```
192
+
193
+ ---
194
+
195
+ ### Task 3: Registration API (TDD)
196
+
197
+ **Files:**
198
+ - Create: `lib/validations/auth.ts`
199
+ - Create: `app/api/auth/register/handler.ts`
200
+ - Create: `app/api/auth/register/route.ts`
201
+ - Test: `tests/api/register.test.ts`
202
+
203
+ **Step 1: Write failing tests**
204
+
205
+ Create `tests/api/register.test.ts`:
206
+ ```ts
207
+ import { describe, it, expect } from "bun:test";
208
+ import { buildRegisterHandler } from "@/app/api/auth/register/handler";
209
+ import { APIError } from "better-auth/api";
210
+
211
+ const makeDeps = () => {
212
+ const cookiesHeader = "session=token; Path=/; HttpOnly";
213
+
214
+ const auth = {
215
+ api: {
216
+ signUpEmail: async () => ({
217
+ headers: new Headers({ "set-cookie": cookiesHeader }),
218
+ response: new Response(JSON.stringify({
219
+ user: { id: "user_1", email: "john@example.com" },
220
+ }))
221
+ })
222
+ }
223
+ };
224
+
225
+ const db = {
226
+ transaction: async (fn: (tx: any) => Promise<void>) => fn({
227
+ insert: () => ({ values: async () => {} })
228
+ })
229
+ };
230
+
231
+ return { auth, db };
232
+ };
233
+
234
+ describe("POST /api/auth/register", () => {
235
+ it("registers a user and sets cookie", async () => {
236
+ const handler = buildRegisterHandler(makeDeps());
237
+ const req = new Request("http://localhost/api/auth/register", {
238
+ method: "POST",
239
+ body: JSON.stringify({ name: "John", email: "john@example.com", password: "Password123" })
240
+ });
241
+
242
+ const res = await handler(req);
243
+ const json = await res.json();
244
+
245
+ expect(res.status).toBe(201);
246
+ expect(json.data.user.email).toBe("john@example.com");
247
+ expect(res.headers.get("set-cookie")).toContain("session=");
248
+ });
249
+
250
+ it("returns 409 when email exists", async () => {
251
+ const deps = makeDeps();
252
+ deps.auth.api.signUpEmail = async () => {
253
+ throw new APIError("Email exists", { status: 409 });
254
+ };
255
+
256
+ const handler = buildRegisterHandler(deps);
257
+ const req = new Request("http://localhost/api/auth/register", {
258
+ method: "POST",
259
+ body: JSON.stringify({ name: "John", email: "john@example.com", password: "Password123" })
260
+ });
261
+
262
+ const res = await handler(req);
263
+ const json = await res.json();
264
+
265
+ expect(res.status).toBe(409);
266
+ expect(json.code).toBe("EMAIL_EXISTS");
267
+ });
268
+
269
+ it("validates email and password", async () => {
270
+ const handler = buildRegisterHandler(makeDeps());
271
+ const req = new Request("http://localhost/api/auth/register", {
272
+ method: "POST",
273
+ body: JSON.stringify({ name: "John", email: "bad-email", password: "weak" })
274
+ });
275
+
276
+ const res = await handler(req);
277
+ const json = await res.json();
278
+
279
+ expect(res.status).toBe(400);
280
+ expect(json.code).toBe("VALIDATION_ERROR");
281
+ });
282
+ });
283
+ ```
284
+
285
+ **Step 2: Run test to verify it fails**
286
+
287
+ Run:
288
+ ```bash
289
+ bun test tests/api/register.test.ts
290
+ ```
291
+ Expected: FAIL because handler/validation do not exist.
292
+
293
+ **Step 3: Implement validation + handler**
294
+
295
+ Create `lib/validations/auth.ts`:
296
+ ```ts
297
+ import { z } from "zod";
298
+
299
+ export const passwordSchema = z
300
+ .string()
301
+ .min(8, "密码至少需要 8 个字符")
302
+ .regex(/[A-Z]/, "密码必须包含大写字母")
303
+ .regex(/[a-z]/, "密码必须包含小写字母")
304
+ .regex(/[0-9!@#$%^&*]/, "密码必须包含数字或特殊字符");
305
+
306
+ export const registerSchema = z.object({
307
+ name: z.string().min(1, "请输入您的姓名").max(100),
308
+ email: z.string().email("请输入有效的邮箱地址").max(255).toLowerCase(),
309
+ password: passwordSchema,
310
+ });
311
+
312
+ export type RegisterInput = z.infer<typeof registerSchema>;
313
+ ```
314
+
315
+ Create `app/api/auth/register/handler.ts`:
316
+ ```ts
317
+ import { NextResponse } from "next/server";
318
+ import { randomUUID } from "crypto";
319
+ import { APIError } from "better-auth/api";
320
+ import { registerSchema } from "@/lib/validations/auth";
321
+ import { generateSlug } from "@/lib/utils/slug";
322
+ import { organizations, organizationMembers, userProfiles } from "@/lib/db/schema";
323
+
324
+ type RegisterDeps = {
325
+ auth: { api: { signUpEmail: (args: any) => Promise<{ headers: Headers; response: Response }> } };
326
+ db: { transaction: <T>(fn: (tx: any) => Promise<T>) => Promise<T> };
327
+ };
328
+
329
+ export function buildRegisterHandler(deps: RegisterDeps) {
330
+ return async function POST(req: Request) {
331
+ try {
332
+ const body = await req.json();
333
+ const parsed = registerSchema.safeParse(body);
334
+ if (!parsed.success) {
335
+ return NextResponse.json(
336
+ {
337
+ error: "Invalid request body",
338
+ code: "VALIDATION_ERROR",
339
+ details: parsed.error.issues,
340
+ },
341
+ { status: 400 }
342
+ );
343
+ }
344
+
345
+ const { name, email, password } = parsed.data;
346
+
347
+ const { headers, response } = await deps.auth.api.signUpEmail({
348
+ returnHeaders: true,
349
+ body: { name, email, password },
350
+ });
351
+
352
+ const authPayload = await response.json();
353
+ const userId = authPayload?.user?.id;
354
+
355
+ if (!userId) {
356
+ return NextResponse.json(
357
+ { error: "Registration failed", code: "REGISTRATION_FAILED" },
358
+ { status: 500 }
359
+ );
360
+ }
361
+
362
+ const orgName = `${name}'s Organization`;
363
+ const orgSlug = generateSlug(orgName);
364
+
365
+ const organizationId = randomUUID();
366
+
367
+ await deps.db.transaction(async (tx) => {
368
+ await tx.insert(userProfiles).values({ userId, name });
369
+ await tx.insert(organizations).values({ id: organizationId, name: orgName, slug: orgSlug });
370
+ await tx.insert(organizationMembers).values({ organizationId, userId, role: "admin" });
371
+ });
372
+
373
+ const res = NextResponse.json(
374
+ {
375
+ data: { user: authPayload.user },
376
+ message: "Registration successful",
377
+ },
378
+ { status: 201 }
379
+ );
380
+
381
+ const setCookie = headers.get("set-cookie");
382
+ if (setCookie) {
383
+ res.headers.set("set-cookie", setCookie);
384
+ }
385
+
386
+ return res;
387
+ } catch (error) {
388
+ if (error instanceof APIError) {
389
+ if (error.status === 409) {
390
+ return NextResponse.json(
391
+ { error: "邮箱已存在", code: "EMAIL_EXISTS" },
392
+ { status: 409 }
393
+ );
394
+ }
395
+
396
+ return NextResponse.json(
397
+ { error: error.message, code: "AUTH_ERROR" },
398
+ { status: error.status ?? 400 }
399
+ );
400
+ }
401
+
402
+ return NextResponse.json(
403
+ { error: "Registration failed", code: "REGISTRATION_FAILED" },
404
+ { status: 500 }
405
+ );
406
+ }
407
+ };
408
+ }
409
+ ```
410
+
411
+ Create `app/api/auth/register/route.ts`:
412
+ ```ts
413
+ import { auth } from "@/lib/auth/config";
414
+ import { db } from "@/lib/db";
415
+ import { buildRegisterHandler } from "./handler";
416
+
417
+ if (!db) {
418
+ throw new Error("DATABASE_URL is not configured");
419
+ }
420
+
421
+ export const POST = buildRegisterHandler({ auth, db });
422
+ ```
423
+
424
+ **Step 4: Run tests to verify pass**
425
+
426
+ Run:
427
+ ```bash
428
+ bun test tests/api/register.test.ts
429
+ ```
430
+ Expected: PASS.
431
+
432
+ **Step 5: Commit**
433
+
434
+ ```bash
435
+ git add lib/validations/auth.ts app/api/auth/register/handler.ts app/api/auth/register/route.ts tests/api/register.test.ts
436
+ git commit -m "feat: add register api with validation"
437
+ ```
438
+
439
+ ---
440
+
441
+ ### Task 4: Register UI
442
+
443
+ **Files:**
444
+ - Create: `app/(auth)/register/page.tsx`
445
+ - Create: `components/auth/register-form.tsx`
446
+
447
+ **Step 1: Implement UI**
448
+
449
+ Create `app/(auth)/register/page.tsx`:
450
+ ```tsx
451
+ import { headers } from "next/headers";
452
+ import { redirect } from "next/navigation";
453
+ import { auth } from "@/lib/auth/config";
454
+ import { RegisterForm } from "@/components/auth/register-form";
455
+
456
+ export default async function RegisterPage() {
457
+ const session = await auth.api.getSession({ headers: await headers() });
458
+ if (session) redirect("/dashboard");
459
+
460
+ return (
461
+ <div className="min-h-screen bg-slate-50 flex items-center justify-center px-4">
462
+ <div className="w-full max-w-md">
463
+ <div className="text-center mb-6">
464
+ <h1 className="text-3xl font-semibold">Echo</h1>
465
+ <p className="text-sm text-muted-foreground">创建新账户以继续</p>
466
+ </div>
467
+ <RegisterForm />
468
+ </div>
469
+ </div>
470
+ );
471
+ }
472
+ ```
473
+
474
+ Create `components/auth/register-form.tsx`:
475
+ ```tsx
476
+ "use client";
477
+
478
+ import { useState } from "react";
479
+ import { useRouter } from "next/navigation";
480
+ import { Button } from "@/components/ui/button";
481
+ import { Input } from "@/components/ui/input";
482
+ import { Label } from "@/components/ui/label";
483
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
484
+
485
+ export function RegisterForm() {
486
+ const router = useRouter();
487
+ const [isLoading, setIsLoading] = useState(false);
488
+ const [errors, setErrors] = useState<Record<string, string>>({});
489
+ const [formData, setFormData] = useState({ name: "", email: "", password: "", confirmPassword: "" });
490
+
491
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
492
+ const { name, value } = e.target;
493
+ setFormData((prev) => ({ ...prev, [name]: value }));
494
+ setErrors((prev) => ({ ...prev, [name]: "" }));
495
+ };
496
+
497
+ const validateForm = () => {
498
+ const nextErrors: Record<string, string> = {};
499
+ if (!formData.name.trim()) nextErrors.name = "请输入您的姓名";
500
+ if (!formData.email) nextErrors.email = "请输入邮箱地址";
501
+ if (!formData.password) nextErrors.password = "请输入密码";
502
+ if (formData.password !== formData.confirmPassword) nextErrors.confirmPassword = "两次输入的密码不一致";
503
+ setErrors(nextErrors);
504
+ return Object.keys(nextErrors).length === 0;
505
+ };
506
+
507
+ const onSubmit = async (e: React.FormEvent) => {
508
+ e.preventDefault();
509
+ if (!validateForm()) return;
510
+
511
+ setIsLoading(true);
512
+ try {
513
+ const res = await fetch("/api/auth/register", {
514
+ method: "POST",
515
+ headers: { "Content-Type": "application/json" },
516
+ body: JSON.stringify({
517
+ name: formData.name,
518
+ email: formData.email,
519
+ password: formData.password,
520
+ }),
521
+ });
522
+
523
+ const json = await res.json();
524
+ if (!res.ok) {
525
+ if (json.code === "VALIDATION_ERROR") {
526
+ const fieldErrors: Record<string, string> = {};
527
+ for (const issue of json.details ?? []) {
528
+ const key = issue.path?.[0];
529
+ if (key) fieldErrors[key] = issue.message;
530
+ }
531
+ setErrors(fieldErrors);
532
+ } else if (json.code === "EMAIL_EXISTS") {
533
+ setErrors({ email: "邮箱已存在" });
534
+ }
535
+ return;
536
+ }
537
+
538
+ router.push("/dashboard");
539
+ } finally {
540
+ setIsLoading(false);
541
+ }
542
+ };
543
+
544
+ return (
545
+ <Card>
546
+ <CardHeader>
547
+ <CardTitle>创建账户</CardTitle>
548
+ <CardDescription>填写以下信息注册新账户</CardDescription>
549
+ </CardHeader>
550
+ <CardContent>
551
+ <form onSubmit={onSubmit} className="space-y-4">
552
+ <div className="space-y-2">
553
+ <Label htmlFor="name">姓名</Label>
554
+ <Input id="name" name="name" value={formData.name} onChange={handleChange} disabled={isLoading} />
555
+ {errors.name ? <p className="text-sm text-destructive">{errors.name}</p> : null}
556
+ </div>
557
+ <div className="space-y-2">
558
+ <Label htmlFor="email">邮箱</Label>
559
+ <Input id="email" name="email" type="email" value={formData.email} onChange={handleChange} disabled={isLoading} />
560
+ {errors.email ? <p className="text-sm text-destructive">{errors.email}</p> : null}
561
+ </div>
562
+ <div className="space-y-2">
563
+ <Label htmlFor="password">密码</Label>
564
+ <Input id="password" name="password" type="password" value={formData.password} onChange={handleChange} disabled={isLoading} />
565
+ {errors.password ? <p className="text-sm text-destructive">{errors.password}</p> : null}
566
+ </div>
567
+ <div className="space-y-2">
568
+ <Label htmlFor="confirmPassword">确认密码</Label>
569
+ <Input id="confirmPassword" name="confirmPassword" type="password" value={formData.confirmPassword} onChange={handleChange} disabled={isLoading} />
570
+ {errors.confirmPassword ? <p className="text-sm text-destructive">{errors.confirmPassword}</p> : null}
571
+ </div>
572
+ <Button type="submit" className="w-full" disabled={isLoading}>
573
+ {isLoading ? "注册中..." : "注册"}
574
+ </Button>
575
+ <p className="text-center text-sm text-muted-foreground">
576
+ 已有账户?<a className="text-primary" href="/login">登录</a>
577
+ </p>
578
+ </form>
579
+ </CardContent>
580
+ </Card>
581
+ );
582
+ }
583
+ ```
584
+
585
+ **Step 2: Manual verification**
586
+
587
+ Run:
588
+ ```bash
589
+ bun dev
590
+ ```
591
+ Check:
592
+ - `/register` renders correctly on mobile/desktop.
593
+ - Invalid inputs show inline errors.
594
+ - Successful submit redirects to `/dashboard`.
595
+
596
+ **Step 3: Commit**
597
+
598
+ ```bash
599
+ git add app/(auth)/register/page.tsx components/auth/register-form.tsx
600
+ git commit -m "feat: add register page and form"
601
+ ```
602
+
603
+ ---
604
+
605
+ ### Task 5: Migration apply + smoke check
606
+
607
+ **Files:**
608
+ - None (commands only)
609
+
610
+ **Step 1: Apply migrations locally**
611
+
612
+ Run:
613
+ ```bash
614
+ bun run db:migrate
615
+ ```
616
+ Expected: migrations applied successfully.
617
+
618
+ **Step 2: Smoke test**
619
+
620
+ Run:
621
+ ```bash
622
+ bun test
623
+ ```
624
+ Expected: all tests pass (existing warning-only lint is acceptable).
625
+
626
+ **Step 3: Commit (if any artifacts)**
627
+
628
+ Only if new migration metadata or files are created.
@@ -0,0 +1,20 @@
1
+ # Roles and Permissions Design
2
+
3
+ **Date:** 2026-01-03
4
+
5
+ ## Goal
6
+ Define a minimal RBAC module with explicit roles and permissions, and align the user schema and validation with the role model.
7
+
8
+ ## Architecture
9
+ We will add a pure, side-effect-free RBAC module at `lib/auth/permissions.ts`. It will export a `UserRole` union type, a `PERMISSIONS` constant map, a `Permission` union derived from the map, and a `ROLE_PERMISSIONS` record that defines which roles can perform each action. The module will expose `hasPermission(role, permission)` and `canSubmitOnBehalf(role)` helpers. The design is intentionally simple: no I/O, no session coupling, and conservative defaults (unknown roles return false). This provides a stable base for middleware and UI gating without over-engineering.
10
+
11
+ To keep data models consistent, we will add a `role` column to the Drizzle `user` table definition in `lib/db/schema/auth.ts`, defaulting to `customer`. We will also add a Zod `userRoleSchema` (and type export) in `lib/validations/auth.ts` so validation and user input handling are aligned with the same role list. The `customer` role represents end users and is granted only `CREATE_FEEDBACK`.
12
+
13
+ ## Data Flow
14
+ Callers pass a role string and permission identifier to the RBAC module. The module returns a boolean. No database or external services are involved.
15
+
16
+ ## Error Handling
17
+ Unknown roles or missing mappings return false. This prevents accidental over-permissioning.
18
+
19
+ ## Testing
20
+ Unit tests will cover role-to-permission mapping, `hasPermission`, and `canSubmitOnBehalf`. Validation tests will assert `userRoleSchema` accepts known roles and rejects unknown ones.