@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,227 @@
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 { NextRequest, NextResponse } from "next/server";
19
+ import { headers } from "next/headers";
20
+ import { eq, and } from "drizzle-orm";
21
+ import { db } from "@/lib/db";
22
+ import { feedback, feedbackTags, tags } from "@/lib/db/schema";
23
+ import { auth } from "@/lib/auth/config";
24
+ import { apiError } from "@/lib/api/errors";
25
+ import { suggestTags } from "@/lib/services/ai/tag-suggester";
26
+ import { getOrgContext } from "@/lib/auth/org-context";
27
+
28
+ export const dynamic = "force-dynamic";
29
+ export const runtime = "nodejs";
30
+
31
+ export async function GET(
32
+ req: NextRequest,
33
+ ctx: { params: Promise<{ id: string }> },
34
+ ) {
35
+ if (!db) {
36
+ return NextResponse.json(
37
+ { error: "Database not configured", code: "DATABASE_NOT_CONFIGURED" },
38
+ { status: 500 },
39
+ );
40
+ }
41
+
42
+ try {
43
+ const { id } = await ctx.params;
44
+ const feedbackId = parseInt(id);
45
+
46
+ if (isNaN(feedbackId)) {
47
+ return NextResponse.json(
48
+ { error: "Invalid feedback ID", code: "INVALID_ID" },
49
+ { status: 400 },
50
+ );
51
+ }
52
+
53
+ const session = await auth.api.getSession({ headers: await headers() });
54
+
55
+ if (!session?.user) {
56
+ return NextResponse.json(
57
+ { error: "Authentication required", code: "UNAUTHORIZED" },
58
+ { status: 401 },
59
+ );
60
+ }
61
+
62
+ let context;
63
+ try {
64
+ context = await getOrgContext({
65
+ request: req,
66
+ db,
67
+ userId: session.user.id,
68
+ requireMembership: true,
69
+ });
70
+ } catch (error) {
71
+ if (error instanceof Error && error.message === "Missing organization") {
72
+ return NextResponse.json(
73
+ { error: "Organization ID is required", code: "MISSING_ORG_ID" },
74
+ { status: 400 },
75
+ );
76
+ }
77
+ return NextResponse.json(
78
+ { error: "Insufficient permissions", code: "FORBIDDEN" },
79
+ { status: 403 },
80
+ );
81
+ }
82
+
83
+ const [feedbackData] = await db
84
+ .select({
85
+ feedbackId: feedback.feedbackId,
86
+ title: feedback.title,
87
+ description: feedback.description,
88
+ })
89
+ .from(feedback)
90
+ .where(
91
+ and(
92
+ eq(feedback.feedbackId, feedbackId),
93
+ eq(feedback.organizationId, context.organizationId),
94
+ ),
95
+ )
96
+ .limit(1);
97
+
98
+ if (!feedbackData) {
99
+ return NextResponse.json(
100
+ { error: "Feedback not found", code: "NOT_FOUND" },
101
+ { status: 404 },
102
+ );
103
+ }
104
+
105
+ const appliedTags = await db
106
+ .select({
107
+ tagId: tags.tagId,
108
+ name: tags.name,
109
+ slug: tags.slug,
110
+ color: tags.color,
111
+ })
112
+ .from(feedbackTags)
113
+ .innerJoin(tags, eq(feedbackTags.tagId, tags.tagId))
114
+ .where(eq(feedbackTags.feedbackId, feedbackId));
115
+
116
+ const suggestions = suggestTags(feedbackData.title, feedbackData.description);
117
+
118
+ const appliedTagSlugs = appliedTags.map((t) => t.slug);
119
+ const filteredSuggestions = suggestions.filter(
120
+ (s) => !appliedTagSlugs.includes(s.slug),
121
+ );
122
+
123
+ return NextResponse.json({
124
+ suggestions: filteredSuggestions,
125
+ applied: appliedTags,
126
+ });
127
+ } catch (error) {
128
+ return apiError(error);
129
+ }
130
+ }
131
+
132
+ export async function POST(
133
+ req: NextRequest,
134
+ ctx: { params: Promise<{ id: string }> },
135
+ ) {
136
+ if (!db) {
137
+ return NextResponse.json(
138
+ { error: "Database not configured", code: "DATABASE_NOT_CONFIGURED" },
139
+ { status: 500 },
140
+ );
141
+ }
142
+
143
+ try {
144
+ const { id } = await ctx.params;
145
+ const feedbackId = parseInt(id);
146
+
147
+ if (isNaN(feedbackId)) {
148
+ return NextResponse.json(
149
+ { error: "Invalid feedback ID", code: "INVALID_ID" },
150
+ { status: 400 },
151
+ );
152
+ }
153
+
154
+ const session = await auth.api.getSession({ headers: await headers() });
155
+
156
+ if (!session?.user) {
157
+ return NextResponse.json(
158
+ { error: "Authentication required", code: "UNAUTHORIZED" },
159
+ { status: 401 },
160
+ );
161
+ }
162
+
163
+ let context;
164
+ try {
165
+ context = await getOrgContext({
166
+ request: req,
167
+ db,
168
+ userId: session.user.id,
169
+ requireMembership: true,
170
+ });
171
+ } catch (error) {
172
+ if (error instanceof Error && error.message === "Missing organization") {
173
+ return NextResponse.json(
174
+ { error: "Organization ID is required", code: "MISSING_ORG_ID" },
175
+ { status: 400 },
176
+ );
177
+ }
178
+ return NextResponse.json(
179
+ { error: "Insufficient permissions", code: "FORBIDDEN" },
180
+ { status: 403 },
181
+ );
182
+ }
183
+
184
+ const body = await req.json();
185
+ const { tagIds } = body;
186
+
187
+ if (!Array.isArray(tagIds)) {
188
+ return NextResponse.json(
189
+ { error: "Invalid tagIds", code: "VALIDATION_ERROR" },
190
+ { status: 400 },
191
+ );
192
+ }
193
+
194
+ const [existingFeedback] = await db
195
+ .select({ feedbackId: feedback.feedbackId })
196
+ .from(feedback)
197
+ .where(
198
+ and(
199
+ eq(feedback.feedbackId, feedbackId),
200
+ eq(feedback.organizationId, context.organizationId),
201
+ ),
202
+ )
203
+ .limit(1);
204
+
205
+ if (!existingFeedback) {
206
+ return NextResponse.json(
207
+ { error: "Feedback not found", code: "NOT_FOUND" },
208
+ { status: 404 },
209
+ );
210
+ }
211
+
212
+ await db.delete(feedbackTags).where(eq(feedbackTags.feedbackId, feedbackId));
213
+
214
+ if (tagIds.length > 0) {
215
+ await db.insert(feedbackTags).values(
216
+ tagIds.map((tagId: number) => ({
217
+ feedbackId,
218
+ tagId,
219
+ })),
220
+ );
221
+ }
222
+
223
+ return NextResponse.json({ success: true });
224
+ } catch (error) {
225
+ return apiError(error);
226
+ }
227
+ }
@@ -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 { NextRequest, NextResponse } from "next/server";
19
+ import { auth } from "@/lib/auth/config";
20
+ import { syncToGitHub } from "@/lib/services/github-sync";
21
+ import { apiError } from "@/lib/api/errors";
22
+
23
+ export const dynamic = "force-dynamic";
24
+ export const runtime = "nodejs";
25
+
26
+ export async function POST(
27
+ req: NextRequest,
28
+ { params }: { params: Promise<{ id: string }> },
29
+ ) {
30
+ const session = await auth.api.getSession({ headers: req.headers });
31
+ if (!session?.user?.id) {
32
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
33
+ }
34
+
35
+ try {
36
+ const { id } = await params;
37
+ const feedbackId = parseInt(id, 10);
38
+
39
+ if (isNaN(feedbackId)) {
40
+ return NextResponse.json(
41
+ { error: "Invalid feedback ID" },
42
+ { status: 400 },
43
+ );
44
+ }
45
+
46
+ await syncToGitHub(feedbackId);
47
+
48
+ return NextResponse.json({ success: true });
49
+ } catch (error) {
50
+ return apiError(error);
51
+ }
52
+ }
@@ -0,0 +1,431 @@
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 { NextRequest, NextResponse } from "next/server";
19
+ import { headers } from "next/headers";
20
+ import { eq, and } from "drizzle-orm";
21
+ import { db } from "@/lib/db";
22
+ import { votes, feedback, organizationSettings } from "@/lib/db/schema";
23
+ import { apiError } from "@/lib/api/errors";
24
+ import { auth } from "@/lib/auth/config";
25
+ import { getOrgContext } from "@/lib/auth/org-context";
26
+
27
+ export const dynamic = "force-dynamic";
28
+ export const runtime = "nodejs";
29
+
30
+ interface RouteParams {
31
+ params: Promise<{ id: string }>;
32
+ }
33
+
34
+ function getClientIp(req: NextRequest): string {
35
+ const forwardedFor = req.headers.get("x-forwarded-for");
36
+ if (forwardedFor) {
37
+ return forwardedFor.split(",")[0].trim();
38
+ }
39
+
40
+ const realIp = req.headers.get("x-real-ip");
41
+ if (realIp) {
42
+ return realIp;
43
+ }
44
+
45
+ return req.headers.get("x-client-ip") || "unknown";
46
+ }
47
+
48
+ function generateVisitorId(ip: string, userAgent: string | null): string {
49
+ const raw = `${ip}-${userAgent || "unknown"}`;
50
+ let hash = 0;
51
+ for (let i = 0; i < raw.length; i++) {
52
+ const char = raw.charCodeAt(i);
53
+ hash = (hash << 5) - hash + char;
54
+ hash = hash & hash;
55
+ }
56
+ return Math.abs(hash).toString(36);
57
+ }
58
+
59
+ export async function GET(req: NextRequest, { params }: RouteParams) {
60
+ if (!db) {
61
+ return NextResponse.json(
62
+ { error: "Database not configured", code: "DATABASE_NOT_CONFIGURED" },
63
+ { status: 500 },
64
+ );
65
+ }
66
+
67
+ try {
68
+ const { id } = await params;
69
+ const feedbackId = parseInt(id);
70
+
71
+ if (isNaN(feedbackId)) {
72
+ return NextResponse.json(
73
+ { error: "Invalid feedback ID", code: "INVALID_ID" },
74
+ { status: 400 },
75
+ );
76
+ }
77
+
78
+ const session = await auth.api.getSession({ headers: await headers() });
79
+ const userId = session?.user?.id ?? null;
80
+ const clientIp = getClientIp(req);
81
+ const userAgent = req.headers.get("user-agent");
82
+ const visitorId = generateVisitorId(clientIp, userAgent);
83
+ let context;
84
+ try {
85
+ context = await getOrgContext({
86
+ request: req,
87
+ db,
88
+ userId,
89
+ requireMembership: !!userId,
90
+ });
91
+ } catch (error) {
92
+ if (error instanceof Error) {
93
+ if (error.message === "Missing organization") {
94
+ return NextResponse.json(
95
+ { error: "Organization ID is required", code: "MISSING_ORG_ID" },
96
+ { status: 400 },
97
+ );
98
+ }
99
+ if (error.message === "Access denied") {
100
+ return NextResponse.json(
101
+ { error: "Insufficient permissions", code: "FORBIDDEN" },
102
+ { status: 403 },
103
+ );
104
+ }
105
+ }
106
+ return apiError(error);
107
+ }
108
+
109
+ const [existingFeedback] = await db
110
+ .select({ feedbackId: feedback.feedbackId, organizationId: feedback.organizationId })
111
+ .from(feedback)
112
+ .where(eq(feedback.feedbackId, feedbackId))
113
+ .limit(1);
114
+
115
+ if (!existingFeedback) {
116
+ return NextResponse.json(
117
+ { error: "Feedback not found", code: "NOT_FOUND" },
118
+ { status: 404 },
119
+ );
120
+ }
121
+ if (existingFeedback.organizationId !== context.organizationId) {
122
+ return NextResponse.json(
123
+ { error: "Feedback not found", code: "NOT_FOUND" },
124
+ { status: 404 },
125
+ );
126
+ }
127
+
128
+ let hasVoted = false;
129
+ let voteId: number | null = null;
130
+
131
+ if (userId) {
132
+ const [userVote] = await db
133
+ .select({ voteId: votes.voteId })
134
+ .from(votes)
135
+ .where(and(eq(votes.feedbackId, feedbackId), eq(votes.userId, userId)))
136
+ .limit(1);
137
+
138
+ hasVoted = !!userVote;
139
+ voteId = userVote?.voteId ?? null;
140
+ } else {
141
+ const [ipVote] = await db
142
+ .select({ voteId: votes.voteId })
143
+ .from(votes)
144
+ .where(
145
+ and(eq(votes.feedbackId, feedbackId), eq(votes.visitorId, visitorId)),
146
+ )
147
+ .limit(1);
148
+
149
+ hasVoted = !!ipVote;
150
+ voteId = ipVote?.voteId ?? null;
151
+ }
152
+
153
+ return NextResponse.json({
154
+ data: {
155
+ hasVoted,
156
+ voteId,
157
+ feedbackId,
158
+ },
159
+ });
160
+ } catch (error) {
161
+ return apiError(error);
162
+ }
163
+ }
164
+
165
+ export async function POST(req: NextRequest, { params }: RouteParams) {
166
+ if (!db) {
167
+ return NextResponse.json(
168
+ { error: "Database not configured", code: "DATABASE_NOT_CONFIGURED" },
169
+ { status: 500 },
170
+ );
171
+ }
172
+
173
+ try {
174
+ const { id } = await params;
175
+ const feedbackId = parseInt(id);
176
+
177
+ if (isNaN(feedbackId)) {
178
+ return NextResponse.json(
179
+ { error: "Invalid feedback ID", code: "INVALID_ID" },
180
+ { status: 400 },
181
+ );
182
+ }
183
+
184
+ const session = await auth.api.getSession({ headers: await headers() });
185
+ const userId = session?.user?.id ?? null;
186
+ const clientIp = getClientIp(req);
187
+ const userAgent = req.headers.get("user-agent");
188
+ const visitorId = generateVisitorId(clientIp, userAgent);
189
+ let context;
190
+ try {
191
+ context = await getOrgContext({
192
+ request: req,
193
+ db,
194
+ userId,
195
+ requireMembership: !!userId,
196
+ });
197
+ } catch (error) {
198
+ if (error instanceof Error) {
199
+ if (error.message === "Missing organization") {
200
+ return NextResponse.json(
201
+ { error: "Organization ID is required", code: "MISSING_ORG_ID" },
202
+ { status: 400 },
203
+ );
204
+ }
205
+ if (error.message === "Access denied") {
206
+ return NextResponse.json(
207
+ { error: "Insufficient permissions", code: "FORBIDDEN" },
208
+ { status: 403 },
209
+ );
210
+ }
211
+ }
212
+ return apiError(error);
213
+ }
214
+
215
+ const [existingFeedback] = await db
216
+ .select({
217
+ feedbackId: feedback.feedbackId,
218
+ deletedAt: feedback.deletedAt,
219
+ organizationId: feedback.organizationId,
220
+ })
221
+ .from(feedback)
222
+ .where(eq(feedback.feedbackId, feedbackId))
223
+ .limit(1);
224
+
225
+ if (!existingFeedback) {
226
+ return NextResponse.json(
227
+ { error: "Feedback not found", code: "NOT_FOUND" },
228
+ { status: 404 },
229
+ );
230
+ }
231
+ if (existingFeedback.organizationId !== context.organizationId) {
232
+ return NextResponse.json(
233
+ { error: "Feedback not found", code: "NOT_FOUND" },
234
+ { status: 404 },
235
+ );
236
+ }
237
+
238
+ if (existingFeedback.deletedAt !== null) {
239
+ return NextResponse.json(
240
+ { error: "Cannot vote on deleted feedback", code: "DELETED" },
241
+ { status: 400 },
242
+ );
243
+ }
244
+
245
+ // Check if public voting is allowed for anonymous users
246
+ if (!userId) {
247
+ const [settings] = await db
248
+ .select({ portalConfig: organizationSettings.portalConfig })
249
+ .from(organizationSettings)
250
+ .where(eq(organizationSettings.organizationId, context.organizationId))
251
+ .limit(1);
252
+
253
+ const allowPublicVoting = settings?.portalConfig?.sharing?.allowPublicVoting ?? true;
254
+
255
+ if (!allowPublicVoting) {
256
+ return NextResponse.json(
257
+ { error: "Public voting is disabled", code: "PUBLIC_VOTING_DISABLED" },
258
+ { status: 403 },
259
+ );
260
+ }
261
+ }
262
+
263
+ if (userId) {
264
+ const [existingVote] = await db
265
+ .select({ voteId: votes.voteId })
266
+ .from(votes)
267
+ .where(and(eq(votes.feedbackId, feedbackId), eq(votes.userId, userId)))
268
+ .limit(1);
269
+
270
+ if (existingVote) {
271
+ return NextResponse.json(
272
+ {
273
+ error: "Already voted",
274
+ code: "ALREADY_VOTED",
275
+ voteId: existingVote.voteId,
276
+ },
277
+ { status: 400 },
278
+ );
279
+ }
280
+ } else {
281
+ const [existingVote] = await db
282
+ .select({ voteId: votes.voteId })
283
+ .from(votes)
284
+ .where(
285
+ and(eq(votes.feedbackId, feedbackId), eq(votes.visitorId, visitorId)),
286
+ )
287
+ .limit(1);
288
+
289
+ if (existingVote) {
290
+ return NextResponse.json(
291
+ {
292
+ error: "Already voted from this device",
293
+ code: "ALREADY_VOTED",
294
+ voteId: existingVote.voteId,
295
+ },
296
+ { status: 400 },
297
+ );
298
+ }
299
+ }
300
+
301
+ const [newVote] = await db
302
+ .insert(votes)
303
+ .values({
304
+ feedbackId,
305
+ userId,
306
+ visitorId: userId ? null : visitorId,
307
+ })
308
+ .returning();
309
+
310
+ return NextResponse.json(
311
+ {
312
+ data: newVote,
313
+ message: "Vote added successfully",
314
+ },
315
+ { status: 201 },
316
+ );
317
+ } catch (error) {
318
+ if (error instanceof Error && error.message.includes("unique constraint")) {
319
+ return NextResponse.json(
320
+ { error: "Already voted", code: "ALREADY_VOTED" },
321
+ { status: 400 },
322
+ );
323
+ }
324
+ return apiError(error);
325
+ }
326
+ }
327
+
328
+ export async function DELETE(req: NextRequest, { params }: RouteParams) {
329
+ if (!db) {
330
+ return NextResponse.json(
331
+ { error: "Database not configured", code: "DATABASE_NOT_CONFIGURED" },
332
+ { status: 500 },
333
+ );
334
+ }
335
+
336
+ try {
337
+ const { id } = await params;
338
+ const feedbackId = parseInt(id);
339
+
340
+ if (isNaN(feedbackId)) {
341
+ return NextResponse.json(
342
+ { error: "Invalid feedback ID", code: "INVALID_ID" },
343
+ { status: 400 },
344
+ );
345
+ }
346
+
347
+ const session = await auth.api.getSession({ headers: await headers() });
348
+ const userId = session?.user?.id ?? null;
349
+ const clientIp = getClientIp(req);
350
+ const userAgent = req.headers.get("user-agent");
351
+ const visitorId = generateVisitorId(clientIp, userAgent);
352
+ let context;
353
+ try {
354
+ context = await getOrgContext({
355
+ request: req,
356
+ db,
357
+ userId,
358
+ requireMembership: !!userId,
359
+ });
360
+ } catch (error) {
361
+ if (error instanceof Error) {
362
+ if (error.message === "Missing organization") {
363
+ return NextResponse.json(
364
+ { error: "Organization ID is required", code: "MISSING_ORG_ID" },
365
+ { status: 400 },
366
+ );
367
+ }
368
+ if (error.message === "Access denied") {
369
+ return NextResponse.json(
370
+ { error: "Insufficient permissions", code: "FORBIDDEN" },
371
+ { status: 403 },
372
+ );
373
+ }
374
+ }
375
+ return apiError(error);
376
+ }
377
+
378
+ const [existingFeedback] = await db
379
+ .select({ feedbackId: feedback.feedbackId, organizationId: feedback.organizationId })
380
+ .from(feedback)
381
+ .where(eq(feedback.feedbackId, feedbackId))
382
+ .limit(1);
383
+
384
+ if (!existingFeedback) {
385
+ return NextResponse.json(
386
+ { error: "Feedback not found", code: "NOT_FOUND" },
387
+ { status: 404 },
388
+ );
389
+ }
390
+ if (existingFeedback.organizationId !== context.organizationId) {
391
+ return NextResponse.json(
392
+ { error: "Feedback not found", code: "NOT_FOUND" },
393
+ { status: 404 },
394
+ );
395
+ }
396
+
397
+ let deletedVote = null;
398
+
399
+ if (userId) {
400
+ const [deleted] = await db
401
+ .delete(votes)
402
+ .where(and(eq(votes.feedbackId, feedbackId), eq(votes.userId, userId)))
403
+ .returning();
404
+
405
+ deletedVote = deleted;
406
+ } else {
407
+ const [deleted] = await db
408
+ .delete(votes)
409
+ .where(
410
+ and(eq(votes.feedbackId, feedbackId), eq(votes.visitorId, visitorId)),
411
+ )
412
+ .returning();
413
+
414
+ deletedVote = deleted;
415
+ }
416
+
417
+ if (!deletedVote) {
418
+ return NextResponse.json(
419
+ { error: "Vote not found", code: "NOT_FOUND" },
420
+ { status: 404 },
421
+ );
422
+ }
423
+
424
+ return NextResponse.json({
425
+ data: deletedVote,
426
+ message: "Vote removed successfully",
427
+ });
428
+ } catch (error) {
429
+ return apiError(error);
430
+ }
431
+ }