@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,424 @@
1
+ /*
2
+ * Copyright (c) 2026 Echo Team
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ import { randomUUID } from "crypto";
19
+ import type { APIRequestContext, Page } from "@playwright/test";
20
+ import { test, expect, TestHelpers, uniqueEmail, uniqueName } from "./helpers/test-utils";
21
+ import { TestDataManager } from "./fixtures/test-data";
22
+
23
+ const password = "StrongPass123!";
24
+ const baseUrl = "http://localhost:3000";
25
+
26
+ type TestUser = {
27
+ email: string;
28
+ name: string;
29
+ password: string;
30
+ };
31
+
32
+ async function registerUser(request: APIRequestContext, user: TestUser) {
33
+ const response = await request.post("/api/auth/register", { data: user });
34
+ if (!response.ok()) {
35
+ throw new Error("Failed to register test user");
36
+ }
37
+ return response.json();
38
+ }
39
+
40
+ async function registerAndLoginUser(
41
+ helpers: TestHelpers,
42
+ request: APIRequestContext,
43
+ user: TestUser,
44
+ ) {
45
+ const json = await registerUser(request, user);
46
+ await helpers.login(user.email, user.password);
47
+ return {
48
+ organizationId: json?.data?.organization?.id as string,
49
+ organizationSlug: json?.data?.organization?.slug as string,
50
+ userId: json?.data?.user?.id as string,
51
+ organizationName: json?.data?.organization?.name as string,
52
+ };
53
+ }
54
+
55
+ async function setOrgCookie(page: Page, organizationId: string) {
56
+ await page.context().addCookies([
57
+ {
58
+ name: "orgId",
59
+ value: organizationId,
60
+ url: baseUrl,
61
+ },
62
+ ]);
63
+ }
64
+
65
+ async function inviteUser(
66
+ request: APIRequestContext,
67
+ organizationId: string,
68
+ email: string,
69
+ role: string,
70
+ ) {
71
+ const response = await request.post(`/api/organizations/${organizationId}/invitations`, {
72
+ data: { email, role },
73
+ });
74
+ if (!response.ok()) {
75
+ throw new Error("Failed to invite user to organization");
76
+ }
77
+ const json = await response.json();
78
+ return json?.data?.token as string;
79
+ }
80
+
81
+ async function acceptInvite(
82
+ request: APIRequestContext,
83
+ token: string,
84
+ ) {
85
+ const response = await request.post("/api/invitations/accept", {
86
+ data: { token },
87
+ });
88
+ if (!response.ok()) {
89
+ throw new Error("Failed to accept invitation");
90
+ }
91
+ }
92
+
93
+ test.describe("E2E-UF-026: Unauthorized access protection", () => {
94
+ test("redirects to login when accessing protected pages without authentication", async ({ page }) => {
95
+ const protectedRoutes = [
96
+ "/dashboard",
97
+ "/settings/profile",
98
+ "/settings/organization",
99
+ "/admin/feedback",
100
+ "/admin/feedback/123/edit",
101
+ ];
102
+
103
+ for (const route of protectedRoutes) {
104
+ await page.goto(route);
105
+ await page.waitForURL(/\/login/);
106
+ await expect(page.locator('input[name="email"], #email')).toBeVisible();
107
+ await expect(page.locator('input[name="password"], #password')).toBeVisible();
108
+ }
109
+ });
110
+
111
+ test("returns 401 for API endpoints without authentication", async ({ request }) => {
112
+ const endpoints = [
113
+ { method: "get", url: "/api/feedback" },
114
+ { method: "get", url: "/api/api-keys" },
115
+ { method: "post", url: "/api/organizations", data: { name: `Unauthorized Org ${Date.now()}` } },
116
+ { method: "post", url: `/api/organizations/${randomUUID()}/invitations`, data: { email: uniqueEmail(), role: "developer" } },
117
+ ] as const;
118
+
119
+ for (const endpoint of endpoints) {
120
+ const response =
121
+ endpoint.method === "get"
122
+ ? await request.get(endpoint.url)
123
+ : await request.post(endpoint.url, { data: endpoint.data });
124
+ expect(response.status()).toBe(401);
125
+ }
126
+ });
127
+
128
+ test("prevents direct URL access to admin pages", async ({ page }) => {
129
+ await page.goto("/admin/feedback");
130
+ await page.waitForURL(/\/login/);
131
+
132
+ await page.goto("/admin/feedback/123/edit");
133
+ await page.waitForURL(/\/login/);
134
+ });
135
+ });
136
+
137
+ test.describe("E2E-UF-027: Role-based feedback permissions", () => {
138
+ let helpers: TestHelpers;
139
+ let testDataManager: TestDataManager;
140
+ let adminUser: TestUser;
141
+ let memberUser: TestUser;
142
+ let supportUser: TestUser;
143
+ let organizationId: string;
144
+ let feedbackId: number;
145
+ let memberInviteToken: string;
146
+ let supportInviteToken: string;
147
+
148
+ test.beforeEach(async ({ page, request }) => {
149
+ helpers = new TestHelpers(page, request);
150
+ testDataManager = new TestDataManager(page.request);
151
+
152
+ adminUser = {
153
+ email: uniqueEmail(),
154
+ name: uniqueName(),
155
+ password,
156
+ };
157
+
158
+ const adminInfo = await registerAndLoginUser(helpers, request, adminUser);
159
+ organizationId = adminInfo.organizationId;
160
+ await setOrgCookie(page, organizationId);
161
+
162
+ const feedback = await testDataManager.createFeedback({
163
+ title: "Test Feedback for Permissions",
164
+ description: "This feedback tests role-based permissions",
165
+ organizationId,
166
+ });
167
+ feedbackId = feedback.feedbackId;
168
+
169
+ memberUser = {
170
+ email: uniqueEmail(),
171
+ name: uniqueName(),
172
+ password,
173
+ };
174
+ supportUser = {
175
+ email: uniqueEmail(),
176
+ name: uniqueName(),
177
+ password,
178
+ };
179
+
180
+ memberInviteToken = await inviteUser(page.request, organizationId, memberUser.email, "developer");
181
+ supportInviteToken = await inviteUser(page.request, organizationId, supportUser.email, "customer_support");
182
+
183
+ await registerUser(request, memberUser);
184
+ await registerUser(request, supportUser);
185
+
186
+ await page.context().clearCookies();
187
+ });
188
+
189
+ test.afterEach(async () => {
190
+ await testDataManager.cleanupAll();
191
+ });
192
+
193
+ test("admin can modify feedback status", async ({ page }) => {
194
+ await helpers.login(adminUser.email, adminUser.password);
195
+ await setOrgCookie(page, organizationId);
196
+
197
+ await page.goto(`/admin/feedback/${feedbackId}`);
198
+
199
+ const statusSelect = page.getByRole("combobox");
200
+ await expect(statusSelect).toBeVisible();
201
+
202
+ await statusSelect.click();
203
+ await page.getByRole("option", { name: "处理中" }).click();
204
+
205
+ await expect(statusSelect).toContainText("处理中");
206
+ });
207
+
208
+ test("member cannot modify feedback status", async ({ page }) => {
209
+ await helpers.login(memberUser.email, memberUser.password);
210
+ await acceptInvite(page.request, memberInviteToken);
211
+ await setOrgCookie(page, organizationId);
212
+
213
+ await page.goto(`/admin/feedback/${feedbackId}`);
214
+ await expect(page.getByRole("combobox")).toHaveCount(0);
215
+ await expect(page.getByText("新接收")).toBeVisible();
216
+
217
+ const response = await page.request.put(`/api/feedback/${feedbackId}`, {
218
+ data: { status: "in-progress" },
219
+ headers: { "x-organization-id": organizationId },
220
+ });
221
+
222
+ expect(response.status()).toBe(403);
223
+ });
224
+
225
+ test("support can only view feedback", async ({ page }) => {
226
+ await helpers.login(supportUser.email, supportUser.password);
227
+ await acceptInvite(page.request, supportInviteToken);
228
+ await setOrgCookie(page, organizationId);
229
+
230
+ await page.goto(`/admin/feedback/${feedbackId}`);
231
+ await expect(page.getByRole("combobox")).toHaveCount(0);
232
+ await expect(page.getByText("新接收")).toBeVisible();
233
+
234
+ const patchResponse = await page.request.put(`/api/feedback/${feedbackId}`, {
235
+ data: { status: "in-progress" },
236
+ headers: { "x-organization-id": organizationId },
237
+ });
238
+ expect(patchResponse.status()).toBe(403);
239
+
240
+ const deleteResponse = await page.request.delete(`/api/feedback/${feedbackId}`, {
241
+ headers: { "x-organization-id": organizationId },
242
+ });
243
+ expect(deleteResponse.status()).toBe(403);
244
+ });
245
+ });
246
+
247
+ test.describe("E2E-UF-028: Organization management permissions", () => {
248
+ let helpers: TestHelpers;
249
+ let adminUser: TestUser;
250
+ let memberUser: TestUser;
251
+ let organizationId: string;
252
+ let adminUserId: string;
253
+ let memberUserId: string;
254
+ let memberInviteToken: string;
255
+
256
+ test.beforeEach(async ({ page, request }) => {
257
+ helpers = new TestHelpers(page, request);
258
+
259
+ adminUser = {
260
+ email: uniqueEmail(),
261
+ name: uniqueName(),
262
+ password,
263
+ };
264
+
265
+ const adminInfo = await registerAndLoginUser(helpers, request, adminUser);
266
+ organizationId = adminInfo.organizationId;
267
+ adminUserId = adminInfo.userId;
268
+ await setOrgCookie(page, organizationId);
269
+
270
+ memberUser = {
271
+ email: uniqueEmail(),
272
+ name: uniqueName(),
273
+ password,
274
+ };
275
+
276
+ memberInviteToken = await inviteUser(page.request, organizationId, memberUser.email, "developer");
277
+ const memberInfo = await registerUser(request, memberUser);
278
+ memberUserId = memberInfo?.data?.user?.id as string;
279
+
280
+ await page.context().clearCookies();
281
+ });
282
+
283
+ test("admins can access organization management", async ({ page }) => {
284
+ await helpers.login(adminUser.email, adminUser.password);
285
+ await setOrgCookie(page, organizationId);
286
+ await page.goto("/settings/organization");
287
+ await expect(page.getByRole("heading", { name: "组织管理" })).toBeVisible();
288
+ });
289
+
290
+ test("only admins can invite members", async ({ page }) => {
291
+ await helpers.login(adminUser.email, adminUser.password);
292
+ await setOrgCookie(page, organizationId);
293
+
294
+ const adminInvite = await page.request.post(`/api/organizations/${organizationId}/invitations`, {
295
+ data: { email: uniqueEmail(), role: "developer" },
296
+ });
297
+ expect(adminInvite.status()).toBe(201);
298
+
299
+ await page.context().clearCookies();
300
+ await helpers.login(memberUser.email, memberUser.password);
301
+ await acceptInvite(page.request, memberInviteToken);
302
+ await setOrgCookie(page, organizationId);
303
+
304
+ const memberInvite = await page.request.post(`/api/organizations/${organizationId}/invitations`, {
305
+ data: { email: uniqueEmail(), role: "developer" },
306
+ });
307
+ expect(memberInvite.status()).toBe(403);
308
+ });
309
+
310
+ test("only admins can remove members", async ({ page }) => {
311
+ await helpers.login(memberUser.email, memberUser.password);
312
+ await acceptInvite(page.request, memberInviteToken);
313
+ await setOrgCookie(page, organizationId);
314
+
315
+ const memberRemove = await page.request.delete(
316
+ `/api/organizations/${organizationId}/members/${adminUserId}`,
317
+ );
318
+ expect(memberRemove.status()).toBe(403);
319
+
320
+ await page.context().clearCookies();
321
+ await helpers.login(adminUser.email, adminUser.password);
322
+ await setOrgCookie(page, organizationId);
323
+
324
+ const adminRemove = await page.request.delete(
325
+ `/api/organizations/${organizationId}/members/${memberUserId}`,
326
+ );
327
+ expect(adminRemove.status()).toBe(200);
328
+ });
329
+
330
+ test("role changes require appropriate permissions", async ({ page }) => {
331
+ await helpers.login(memberUser.email, memberUser.password);
332
+ await acceptInvite(page.request, memberInviteToken);
333
+ await setOrgCookie(page, organizationId);
334
+
335
+ const memberChange = await page.request.put(
336
+ `/api/organizations/${organizationId}/members/${memberUserId}`,
337
+ { data: { role: "product_manager" } },
338
+ );
339
+ expect(memberChange.status()).toBe(403);
340
+ });
341
+ });
342
+
343
+ test.describe("Permission boundary testing", () => {
344
+ test("users cannot access other organizations' data", async ({ page, request }) => {
345
+ const helpers = new TestHelpers(page, request);
346
+ const testDataManager = new TestDataManager(page.request);
347
+
348
+ const user1: TestUser = {
349
+ email: uniqueEmail(),
350
+ name: uniqueName(),
351
+ password,
352
+ };
353
+
354
+ const user2: TestUser = {
355
+ email: uniqueEmail(),
356
+ name: uniqueName(),
357
+ password,
358
+ };
359
+
360
+ const user1Info = await registerAndLoginUser(helpers, request, user1);
361
+ await setOrgCookie(page, user1Info.organizationId);
362
+
363
+ const feedback = await testDataManager.createFeedback({
364
+ title: "Org 1 Feedback",
365
+ description: "This belongs to org 1",
366
+ organizationId: user1Info.organizationId,
367
+ });
368
+
369
+ await page.context().clearCookies();
370
+ const user2Info = await registerAndLoginUser(helpers, request, user2);
371
+ await setOrgCookie(page, user2Info.organizationId);
372
+
373
+ const response = await page.request.get(`/api/feedback/${feedback.feedbackId}`, {
374
+ headers: { "x-organization-id": user2Info.organizationId },
375
+ });
376
+ expect(response.status()).toBe(404);
377
+
378
+ await testDataManager.cleanupAll();
379
+ });
380
+
381
+ test("session isolation between users", async ({ context, request }) => {
382
+ const browserContext = await context.browser();
383
+ if (!browserContext) throw new Error("Browser context not available");
384
+
385
+ const context1 = await browserContext.newContext();
386
+ const context2 = await browserContext.newContext();
387
+
388
+ const page1 = await context1.newPage();
389
+ const page2 = await context2.newPage();
390
+
391
+ const helpers1 = new TestHelpers(page1, request);
392
+ const helpers2 = new TestHelpers(page2, request);
393
+
394
+ const user1: TestUser = {
395
+ email: uniqueEmail(),
396
+ name: uniqueName(),
397
+ password,
398
+ };
399
+
400
+ const user2: TestUser = {
401
+ email: uniqueEmail(),
402
+ name: uniqueName(),
403
+ password,
404
+ };
405
+
406
+ const user1Info = await registerUser(request, user1);
407
+ const user2Info = await registerUser(request, user2);
408
+
409
+ await helpers1.login(user1.email, user1.password);
410
+ await helpers2.login(user2.email, user2.password);
411
+
412
+ const user1OrgName = user1Info?.data?.organization?.name as string;
413
+ const user2OrgName = user2Info?.data?.organization?.name as string;
414
+
415
+ await expect(page1.getByRole("combobox")).toContainText(user1OrgName);
416
+ await expect(page2.getByRole("combobox")).toContainText(user2OrgName);
417
+
418
+ await expect(page1.getByRole("combobox")).not.toContainText(user2OrgName);
419
+ await expect(page2.getByRole("combobox")).not.toContainText(user1OrgName);
420
+
421
+ await context1.close();
422
+ await context2.close();
423
+ });
424
+ });
@@ -0,0 +1,63 @@
1
+ /*
2
+ * Copyright (c) 2026 Echo Team
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ import { test, expect, TestHelpers, uniqueEmail, uniqueName } from "./helpers/test-utils";
19
+
20
+ test.describe("E2E-UF-021: Widget settings", () => {
21
+ let helpers: TestHelpers;
22
+
23
+ test.beforeEach(async ({ page, request }) => {
24
+ helpers = new TestHelpers(page, request);
25
+
26
+ const name = uniqueName();
27
+ const email = uniqueEmail();
28
+ await helpers.registerAndLogin(name, email, "TestPass123!");
29
+ });
30
+
31
+ test("shows widgets page and coming soon state", async ({ page }) => {
32
+ await page.goto("/settings/widgets");
33
+
34
+ await expect(page.getByRole("heading", { name: /widgets & embeds/i })).toBeVisible();
35
+ await expect(page.getByText(/Widget configuration coming soon/i)).toBeVisible();
36
+ await expect(page.getByText(/Changelog widget coming soon/i)).toBeVisible();
37
+ });
38
+ });
39
+
40
+ test.describe("E2E-UF-022: Public widget", () => {
41
+ let helpers: TestHelpers;
42
+ let organizationId: string;
43
+ let organizationName: string;
44
+
45
+ test.beforeEach(async ({ page, request }) => {
46
+ helpers = new TestHelpers(page, request);
47
+
48
+ const name = uniqueName();
49
+ const email = uniqueEmail();
50
+ await helpers.registerAndLogin(name, email, "TestPass123!");
51
+
52
+ organizationId = helpers.getOrganizationId();
53
+ organizationName = `${name}'s Organization`;
54
+ });
55
+
56
+ test("renders widget for organization", async ({ page }) => {
57
+ await page.goto(`/widget/${organizationId}`);
58
+
59
+ await expect(page.getByRole("heading", { name: /feedback/i })).toBeVisible();
60
+ await expect(page.getByText(organizationName)).toBeVisible();
61
+ await expect(page.getByText(/We'd love to hear your thoughts/i)).toBeVisible();
62
+ });
63
+ });
@@ -0,0 +1,29 @@
1
+ /*
2
+ * Copyright (c) 2026 Echo Team
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ import { describe, expect, it } from "bun:test";
19
+ import { parseCsvParam, serializeCsvParam } from "@/lib/feedback/filters";
20
+
21
+ describe("filters helpers", () => {
22
+ it("parses csv into array", () => {
23
+ expect(parseCsvParam("a,b")).toEqual(["a", "b"]);
24
+ });
25
+
26
+ it("serializes array into csv", () => {
27
+ expect(serializeCsvParam(["a", "b"])).toBe("a,b");
28
+ });
29
+ });
@@ -0,0 +1,52 @@
1
+ /*
2
+ * Copyright (c) 2026 Echo Team
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ import { describe, expect, it } from "bun:test";
19
+ import { PERMISSIONS } from "@/lib/auth/permissions";
20
+ import { useCan, useHasPermission } from "@/hooks/use-permissions";
21
+
22
+ describe("use-permissions", () => {
23
+ it("returns false when session has no role", () => {
24
+ expect(useCan(PERMISSIONS.CREATE_FEEDBACK, null)).toBe(false);
25
+ expect(useHasPermission(PERMISSIONS.CREATE_FEEDBACK, null)).toBe(false);
26
+ });
27
+
28
+ it("checks permissions for the current role", () => {
29
+ const session = { user: { role: "admin" } } as {
30
+ user: { role: string };
31
+ };
32
+
33
+ expect(useCan(PERMISSIONS.MANAGE_ORG, session)).toBe(true);
34
+ expect(useCan(PERMISSIONS.SUBMIT_ON_BEHALF, session)).toBe(true);
35
+ });
36
+
37
+ it("requires all permissions when passed a list", () => {
38
+ const session = { user: { role: "developer" } } as {
39
+ user: { role: string };
40
+ };
41
+
42
+ expect(
43
+ useHasPermission(
44
+ [
45
+ PERMISSIONS.CREATE_FEEDBACK,
46
+ PERMISSIONS.DELETE_FEEDBACK,
47
+ ],
48
+ session,
49
+ ),
50
+ ).toBe(false);
51
+ });
52
+ });
@@ -0,0 +1,104 @@
1
+ /*
2
+ * Copyright (c) 2026 Echo Team
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ import { describe, it, expect } from "vitest";
19
+ import {
20
+ classifyFeedback,
21
+ classifyType,
22
+ classifyPriority,
23
+ } from "@/lib/services/ai/classifier";
24
+
25
+ describe("AI Classifier", () => {
26
+ describe("classifyType", () => {
27
+ it("should classify bugs correctly", () => {
28
+ expect(classifyType("App crashes on startup")).toBe("bug");
29
+ expect(classifyType("无法登录系统")).toBe("bug");
30
+ expect(classifyType("Error 500 when saving")).toBe("bug");
31
+ });
32
+
33
+ it("should classify features correctly", () => {
34
+ expect(classifyType("希望添加导出功能")).toBe("feature");
35
+ expect(classifyType("Suggest adding dark mode")).toBe("feature");
36
+ expect(classifyType("Would like to see search")).toBe("feature");
37
+ });
38
+
39
+ it("should classify issues correctly", () => {
40
+ expect(classifyType("How to use the app?")).toBe("issue");
41
+ expect(classifyType("问题:如何重置密码")).toBe("issue");
42
+ expect(classifyType("求助:不清楚怎么操作")).toBe("issue");
43
+ });
44
+
45
+ it("should default to other for unclear content", () => {
46
+ expect(classifyType("Just saying hello")).toBe("other");
47
+ expect(classifyType("Thanks for the great app")).toBe("other");
48
+ });
49
+ });
50
+
51
+ describe("classifyPriority", () => {
52
+ it("should classify high priority correctly", () => {
53
+ expect(classifyPriority("紧急:系统无法使用")).toBe("high");
54
+ expect(classifyPriority("Critical bug blocking all users")).toBe("high");
55
+ });
56
+
57
+ it("should classify low priority correctly", () => {
58
+ expect(classifyPriority("建议:改进颜色搭配")).toBe("low");
59
+ expect(classifyPriority("Nice to have feature")).toBe("low");
60
+ });
61
+
62
+ it("should default to medium priority", () => {
63
+ expect(classifyPriority("Something broke")).toBe("medium");
64
+ });
65
+ });
66
+
67
+ describe("classifyFeedback", () => {
68
+ it("should return classification with confidence", () => {
69
+ const result = classifyFeedback(
70
+ "App崩溃无法使用",
71
+ "点击按钮后应用闪退,完全无法使用",
72
+ );
73
+
74
+ expect(result.type).toBe("bug");
75
+ expect(result.priority).toBe("high");
76
+ expect(result.confidence).toBeGreaterThan(0);
77
+ expect(result.reasons.length).toBeGreaterThan(0);
78
+ });
79
+
80
+ it("should provide reasons for classification", () => {
81
+ const result = classifyFeedback("希望添加导出功能");
82
+
83
+ expect(result.reasons).toEqual(
84
+ expect.arrayContaining([expect.stringContaining("希望")]),
85
+ );
86
+ });
87
+
88
+ it("should handle empty description", () => {
89
+ const result = classifyFeedback("Bug in login page");
90
+
91
+ expect(result.type).toBe("bug");
92
+ expect(result.confidence).toBeGreaterThan(0);
93
+ });
94
+
95
+ it("should limit reasons to 3", () => {
96
+ const result = classifyFeedback(
97
+ "崩溃错误无法失败异常",
98
+ "故障不能不工作没反应卡住卡死闪退",
99
+ );
100
+
101
+ expect(result.reasons.length).toBeLessThanOrEqual(3);
102
+ });
103
+ });
104
+ });