@nexttylabs/echo 0.3.0 → 0.5.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 (248) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/app/(public)/[organizationSlug]/roadmap/page.tsx +19 -1
  3. package/app/api/admin/backup/route.ts +22 -4
  4. package/app/api/auth/register/handler.ts +1 -2
  5. package/lib/auth/config.ts +0 -7
  6. package/lib/db/migrations/0000_needy_leech.sql +335 -0
  7. package/lib/db/migrations/meta/0000_snapshot.json +2186 -1
  8. package/lib/db/migrations/meta/_journal.json +2 -135
  9. package/lib/db/schema/auth.ts +0 -1
  10. package/lib/db/schema/index.ts +0 -1
  11. package/lib/portal/public-context.tsx +5 -0
  12. package/package.json +20 -1
  13. package/.changeset/README.md +0 -21
  14. package/.changeset/config.json +0 -11
  15. package/.changeset/cozy-ghosts-care.md +0 -5
  16. package/.changeset/sharp-lines-stand.md +0 -5
  17. package/.changeset/sour-doodles-eat.md +0 -5
  18. package/.changeset/tender-moose-shop.md +0 -5
  19. package/.github/pull_request_template.md +0 -13
  20. package/.github/workflows/ci.yml +0 -41
  21. package/.github/workflows/publish.yml +0 -44
  22. package/.github/workflows/release.yml +0 -73
  23. package/AGENTS.md +0 -92
  24. package/Dockerfile +0 -57
  25. package/Makefile +0 -77
  26. package/app/api/internal/domain-lookup/route.ts +0 -67
  27. package/bun.lock +0 -2503
  28. package/components/portal/project-switcher.tsx +0 -20
  29. package/docker-compose.dev.yml +0 -26
  30. package/docker-compose.yml +0 -98
  31. package/docs/architecture.md +0 -259
  32. package/docs/component-inventory.md +0 -261
  33. package/docs/database-migrations.md +0 -76
  34. package/docs/development-guide.md +0 -209
  35. package/docs/e2e-user-flows.csv +0 -31
  36. package/docs/er-diagram-feedback.mmd +0 -138
  37. package/docs/er-diagram.mmd +0 -281
  38. package/docs/i18n-check-report.md +0 -296
  39. package/docs/index.md +0 -214
  40. package/docs/logic-chain.md +0 -94
  41. package/docs/plans/2026-01-02-database-migration-scripts.md +0 -496
  42. package/docs/plans/2026-01-02-user-login-design.md +0 -37
  43. package/docs/plans/2026-01-02-user-login.md +0 -437
  44. package/docs/plans/2026-01-02-user-registration-design.md +0 -47
  45. package/docs/plans/2026-01-02-user-registration.md +0 -628
  46. package/docs/plans/2026-01-03-roles-permissions-design.md +0 -20
  47. package/docs/plans/2026-01-03-roles-permissions.md +0 -266
  48. package/docs/plans/2026-01-05-authentication-middleware.md +0 -207
  49. package/docs/plans/2026-01-05-member-removal.md +0 -186
  50. package/docs/plans/2026-01-05-organization-creation.md +0 -374
  51. package/docs/plans/2026-01-05-rbac-middleware.md +0 -112
  52. package/docs/plans/2026-01-05-role-configuration.md +0 -441
  53. package/docs/plans/2026-01-06-file-upload-support.md +0 -804
  54. package/docs/plans/2026-01-06-permission-check-hook.md +0 -155
  55. package/docs/plans/2026-01-06-resource-ownership-check.md +0 -231
  56. package/docs/plans/2026-01-07-feedback-tracking-link.md +0 -459
  57. package/docs/plans/2026-01-09-logout-redirect-design.md +0 -52
  58. package/docs/plans/2026-01-09-phase2-3-plan.md +0 -654
  59. package/docs/plans/2026-01-09-portal-execution-plan.md +0 -408
  60. package/docs/plans/2026-01-09-project-delete-feature-design.md +0 -163
  61. package/docs/plans/2026-01-09-project-delete-implementation.md +0 -451
  62. package/docs/plans/2026-01-09-project-edit-delete-design.md +0 -52
  63. package/docs/plans/2026-01-09-settings-center-design.md +0 -114
  64. package/docs/plans/2026-01-09-settings-center.md +0 -948
  65. package/docs/plans/2026-01-10-organization-only-design.md +0 -66
  66. package/docs/plans/2026-01-10-organization-only-implementation.md +0 -433
  67. package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +0 -18
  68. package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +0 -296
  69. package/docs/plans/2026-01-14-e2e-playwright-feedback.md +0 -173
  70. package/docs/plans/2026-01-15-feedback-management-org-context-design.md +0 -82
  71. package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +0 -521
  72. package/docs/plans/2026-01-16-admin-feedback-filters-design.md +0 -75
  73. package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +0 -293
  74. package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +0 -180
  75. package/docs/plans/2026-01-16-e2e-test-fixes.md +0 -158
  76. package/docs/plans/2026-01-17-admin-feedback-filters.md +0 -214
  77. package/docs/plans/2026-01-17-admin-feedback-improvements.md +0 -453
  78. package/docs/plans/2026-01-18-changesets-design.md +0 -40
  79. package/docs/product_changes.md +0 -37
  80. package/docs/project-overview.md +0 -159
  81. package/docs/project-scan-report.json +0 -104
  82. package/docs/route-role-visibility.md +0 -51
  83. package/docs/source-tree-analysis.md +0 -150
  84. package/docs/testing/delete-project-manual-tests.md +0 -18
  85. package/docs/user-story-tracking.md +0 -191
  86. package/eslint.config.mjs +0 -19
  87. package/lib/db/migrations/.gitkeep +0 -0
  88. package/lib/db/migrations/0000_cynical_gladiator.sql +0 -53
  89. package/lib/db/migrations/0001_wandering_sunfire.sql +0 -27
  90. package/lib/db/migrations/0002_shallow_speedball.sql +0 -1
  91. package/lib/db/migrations/0003_add_org_description.sql +0 -1
  92. package/lib/db/migrations/0003_boring_wild_pack.sql +0 -13
  93. package/lib/db/migrations/0004_windy_tyrannus.sql +0 -27
  94. package/lib/db/migrations/0005_perpetual_doorman.sql +0 -5
  95. package/lib/db/migrations/0006_aberrant_captain_midlands.sql +0 -13
  96. package/lib/db/migrations/0007_clever_captain_cross.sql +0 -14
  97. package/lib/db/migrations/0008_sparkling_pandemic.sql +0 -2
  98. package/lib/db/migrations/0009_happy_black_tom.sql +0 -29
  99. package/lib/db/migrations/0010_kind_junta.sql +0 -8
  100. package/lib/db/migrations/0011_mute_squadron_supreme.sql +0 -25
  101. package/lib/db/migrations/0012_giant_power_man.sql +0 -24
  102. package/lib/db/migrations/0013_damp_titanium_man.sql +0 -17
  103. package/lib/db/migrations/0014_blue_alice.sql +0 -18
  104. package/lib/db/migrations/0015_webhook_tables.sql +0 -41
  105. package/lib/db/migrations/0016_github_integration.sql +0 -30
  106. package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +0 -22
  107. package/lib/db/migrations/0017_slimy_inhumans.sql +0 -6
  108. package/lib/db/migrations/0018_same_spitfire.sql +0 -1
  109. package/lib/db/migrations/0019_jittery_loners.sql +0 -16
  110. package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +0 -14
  111. package/lib/db/migrations/meta/0001_snapshot.json +0 -553
  112. package/lib/db/migrations/meta/0002_snapshot.json +0 -560
  113. package/lib/db/migrations/meta/0003_snapshot.json +0 -650
  114. package/lib/db/migrations/meta/0004_snapshot.json +0 -852
  115. package/lib/db/migrations/meta/0005_snapshot.json +0 -900
  116. package/lib/db/migrations/meta/0006_snapshot.json +0 -1011
  117. package/lib/db/migrations/meta/0007_snapshot.json +0 -1125
  118. package/lib/db/migrations/meta/0008_snapshot.json +0 -1146
  119. package/lib/db/migrations/meta/0009_snapshot.json +0 -1386
  120. package/lib/db/migrations/meta/0010_snapshot.json +0 -1419
  121. package/lib/db/migrations/meta/0011_snapshot.json +0 -1615
  122. package/lib/db/migrations/meta/0012_snapshot.json +0 -1805
  123. package/lib/db/migrations/meta/0013_snapshot.json +0 -1948
  124. package/lib/db/migrations/meta/0014_snapshot.json +0 -2082
  125. package/lib/db/migrations/meta/0015_snapshot.json +0 -2476
  126. package/lib/db/migrations/meta/0016_snapshot.json +0 -2633
  127. package/lib/db/migrations/meta/0017_snapshot.json +0 -2680
  128. package/lib/db/migrations/meta/0018_snapshot.json +0 -2686
  129. package/lib/db/migrations/meta/0019_snapshot.json +0 -2741
  130. package/lib/db/schema/projects.ts +0 -145
  131. package/lib/db/schema/user-profiles.ts +0 -31
  132. package/lib/validations/projects.ts +0 -49
  133. package/next-env.d.ts +0 -6
  134. package/playwright.config.ts +0 -44
  135. package/proxy.test.ts +0 -131
  136. package/proxy.ts +0 -190
  137. package/scripts/backup-db.sh +0 -57
  138. package/scripts/backup-db.ts +0 -24
  139. package/scripts/generate-openapi.ts +0 -22
  140. package/scripts/migration-helper.ts +0 -39
  141. package/scripts/pre-deploy.ts +0 -75
  142. package/scripts/restore-db.sh +0 -60
  143. package/scripts/rollback.ts +0 -72
  144. package/scripts/seed-tags.ts +0 -48
  145. package/tests/api/feedback-bulk.test.ts +0 -47
  146. package/tests/api/feedback-by-id.test.ts +0 -67
  147. package/tests/api/feedback-comments-route-import.test.ts +0 -26
  148. package/tests/api/feedback-create.test.ts +0 -71
  149. package/tests/api/feedback-delete.test.ts +0 -160
  150. package/tests/api/feedback-filter.test.ts +0 -250
  151. package/tests/api/feedback-list.test.ts +0 -234
  152. package/tests/api/feedback-route-assignee-condition.test.ts +0 -32
  153. package/tests/api/feedback-similar.test.ts +0 -46
  154. package/tests/api/feedback-sort.test.ts +0 -261
  155. package/tests/api/feedback-status-enum.test.ts +0 -49
  156. package/tests/api/feedback-status-filter.test.ts +0 -117
  157. package/tests/api/feedback-submit-on-behalf.test.ts +0 -269
  158. package/tests/api/feedback.test.ts +0 -175
  159. package/tests/api/identify-jwt.test.ts +0 -25
  160. package/tests/api/invitation-accept.test.ts +0 -213
  161. package/tests/api/organization-invitations.test.ts +0 -186
  162. package/tests/api/organization-members-list.test.ts +0 -79
  163. package/tests/api/organization-members.test.ts +0 -340
  164. package/tests/api/organizations.test.ts +0 -149
  165. package/tests/api/register.test.ts +0 -112
  166. package/tests/api/upload.test.ts +0 -103
  167. package/tests/api/vote.test.ts +0 -82
  168. package/tests/app/admin-feedback-detail-page.test.tsx +0 -25
  169. package/tests/app/admin-feedback-list-page.test.tsx +0 -25
  170. package/tests/app/admin-feedback-new-page.test.tsx +0 -25
  171. package/tests/app/health-route-helpers.test.ts +0 -27
  172. package/tests/app/login-page.test.ts +0 -26
  173. package/tests/app/portal-page.test.ts +0 -29
  174. package/tests/app/project-portal-overview.test.tsx +0 -25
  175. package/tests/app/widget-page-import.test.ts +0 -25
  176. package/tests/components/create-post-dialog-defaults.test.ts +0 -43
  177. package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +0 -27
  178. package/tests/components/feedback/embedded-feedback-form.test.tsx +0 -96
  179. package/tests/components/feedback/feedback-detail.test.tsx +0 -25
  180. package/tests/components/feedback/feedback-stats.test.tsx +0 -49
  181. package/tests/components/feedback-bulk-actions.test.tsx +0 -39
  182. package/tests/components/feedback-i18n-keys.test.ts +0 -70
  183. package/tests/components/feedback-list-controls-compile.test.ts +0 -25
  184. package/tests/components/feedback-list-controls.test.tsx +0 -204
  185. package/tests/components/feedback-list-item.test.tsx +0 -67
  186. package/tests/components/landing/hero.test.tsx +0 -46
  187. package/tests/components/layout/language-switcher.test.tsx +0 -25
  188. package/tests/components/layout/sidebar.test.tsx +0 -157
  189. package/tests/components/login-form.test.ts +0 -25
  190. package/tests/components/organization-form.test.ts +0 -32
  191. package/tests/components/organization-switcher.test.ts +0 -25
  192. package/tests/components/pagination.test.tsx +0 -43
  193. package/tests/components/portal-overview.test.tsx +0 -25
  194. package/tests/components/profile-form.test.tsx +0 -139
  195. package/tests/components/role-selector.test.ts +0 -31
  196. package/tests/components/status-chart.test.tsx +0 -90
  197. package/tests/e2e/auth.e2e.ts +0 -323
  198. package/tests/e2e/feedback-actions.e2e.ts +0 -471
  199. package/tests/e2e/feedback-attachment.e2e.ts +0 -168
  200. package/tests/e2e/feedback-customer.e2e.ts +0 -226
  201. package/tests/e2e/feedback-management.e2e.ts +0 -565
  202. package/tests/e2e/feedback-submit.e2e.ts +0 -133
  203. package/tests/e2e/feedback-view.e2e.ts +0 -297
  204. package/tests/e2e/fixtures/test-data.ts +0 -235
  205. package/tests/e2e/health-check.e2e.ts +0 -230
  206. package/tests/e2e/helpers/test-utils-helpers.test.ts +0 -43
  207. package/tests/e2e/helpers/test-utils.ts +0 -298
  208. package/tests/e2e/integration-placeholders.e2e.ts +0 -199
  209. package/tests/e2e/organization.e2e.ts +0 -292
  210. package/tests/e2e/permissions.e2e.ts +0 -424
  211. package/tests/e2e/project-widget.e2e.ts +0 -63
  212. package/tests/feedback/filters.test.ts +0 -29
  213. package/tests/hooks/use-permissions.test.ts +0 -52
  214. package/tests/lib/ai/classifier.test.ts +0 -104
  215. package/tests/lib/ai/duplicate-detector.test.ts +0 -234
  216. package/tests/lib/attachments-schema.test.ts +0 -30
  217. package/tests/lib/auth/session.test.ts +0 -49
  218. package/tests/lib/auth-client.test.ts +0 -37
  219. package/tests/lib/auth-config.test.ts +0 -26
  220. package/tests/lib/feedback-prefill.test.ts +0 -52
  221. package/tests/lib/feedback-processor.test.ts +0 -41
  222. package/tests/lib/feedback-schema.test.ts +0 -33
  223. package/tests/lib/file-validator.test.ts +0 -48
  224. package/tests/lib/get-feedback-by-id.test.ts +0 -37
  225. package/tests/lib/invitations.test.ts +0 -35
  226. package/tests/lib/login-schema.test.ts +0 -36
  227. package/tests/lib/org-context.test.ts +0 -95
  228. package/tests/lib/organization-access.test.ts +0 -44
  229. package/tests/lib/organization-member-role-schema.test.ts +0 -41
  230. package/tests/lib/permissions.test.ts +0 -88
  231. package/tests/lib/portal-analytics.test.ts +0 -25
  232. package/tests/lib/portal-contributors.test.ts +0 -25
  233. package/tests/lib/portal-copy.test.ts +0 -27
  234. package/tests/lib/portal-i18n.test.ts +0 -30
  235. package/tests/lib/portal-leaderboard-settings.test.ts +0 -25
  236. package/tests/lib/portal-modules.test.ts +0 -25
  237. package/tests/lib/portal-seo.test.ts +0 -25
  238. package/tests/lib/portal-sharing.test.ts +0 -25
  239. package/tests/lib/portal-sorting.test.ts +0 -25
  240. package/tests/lib/portal-theme.test.ts +0 -25
  241. package/tests/lib/rate-limit.test.ts +0 -142
  242. package/tests/lib/resolve-locale.test.ts +0 -34
  243. package/tests/lib/services/backup.test.ts +0 -145
  244. package/tests/lib/user-organizations.test.ts +0 -42
  245. package/tests/lib/user-role-schema.test.ts +0 -33
  246. package/tests/lib/user-schema.test.ts +0 -25
  247. package/tests/setup.ts +0 -74
  248. package/vercel.json +0 -4
@@ -1,424 +0,0 @@
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
- });
@@ -1,63 +0,0 @@
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
- });
@@ -1,29 +0,0 @@
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
- });
@@ -1,52 +0,0 @@
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
- });
@@ -1,104 +0,0 @@
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
- });