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