@nexttylabs/echo 0.4.0 → 0.6.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 (262) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/app/(dashboard)/admin/feedback/[id]/edit/page.tsx +12 -6
  3. package/app/(dashboard)/admin/feedback/new/page.tsx +19 -17
  4. package/app/(dashboard)/admin/layout.tsx +16 -6
  5. package/app/(dashboard)/layout.tsx +4 -2
  6. package/app/(dashboard)/settings/api-keys/page.tsx +13 -3
  7. package/app/(dashboard)/settings/layout.tsx +25 -2
  8. package/app/(dashboard)/settings/organization/page.tsx +8 -9
  9. package/app/(public)/[organizationSlug]/roadmap/page.tsx +19 -1
  10. package/app/api/admin/backup/route.ts +22 -4
  11. package/app/api/auth/register/handler.ts +1 -2
  12. package/app/api/feedback/[id]/comments/[commentId]/route.ts +13 -4
  13. package/app/api/feedback/[id]/reclassify/route.ts +4 -4
  14. package/app/api/organizations/handler.ts +2 -4
  15. package/components/settings/settings-sidebar.tsx +4 -4
  16. package/hooks/use-organization.tsx +116 -0
  17. package/hooks/use-permissions.ts +24 -11
  18. package/lib/auth/config.ts +0 -7
  19. package/lib/auth/organization.ts +20 -0
  20. package/lib/auth/permissions.ts +10 -0
  21. package/lib/db/migrations/0000_needy_leech.sql +335 -0
  22. package/lib/db/migrations/meta/0000_snapshot.json +2186 -1
  23. package/lib/db/migrations/meta/_journal.json +2 -135
  24. package/lib/db/schema/auth.ts +0 -1
  25. package/lib/db/schema/index.ts +0 -1
  26. package/lib/portal/public-context.tsx +5 -0
  27. package/package.json +20 -1
  28. package/.changeset/README.md +0 -21
  29. package/.changeset/config.json +0 -11
  30. package/.changeset/cozy-ghosts-care.md +0 -5
  31. package/.changeset/sharp-lines-stand.md +0 -5
  32. package/.changeset/sour-doodles-eat.md +0 -5
  33. package/.changeset/tender-moose-shop.md +0 -5
  34. package/.github/pull_request_template.md +0 -13
  35. package/.github/workflows/ci.yml +0 -41
  36. package/.github/workflows/publish.yml +0 -44
  37. package/.github/workflows/release.yml +0 -73
  38. package/AGENTS.md +0 -92
  39. package/Dockerfile +0 -57
  40. package/Makefile +0 -77
  41. package/bun.lock +0 -2503
  42. package/components/portal/project-switcher.tsx +0 -20
  43. package/docker-compose.dev.yml +0 -26
  44. package/docker-compose.yml +0 -98
  45. package/docs/architecture.md +0 -259
  46. package/docs/component-inventory.md +0 -261
  47. package/docs/database-migrations.md +0 -76
  48. package/docs/development-guide.md +0 -209
  49. package/docs/e2e-user-flows.csv +0 -31
  50. package/docs/er-diagram-feedback.mmd +0 -138
  51. package/docs/er-diagram.mmd +0 -281
  52. package/docs/i18n-check-report.md +0 -296
  53. package/docs/index.md +0 -214
  54. package/docs/logic-chain.md +0 -94
  55. package/docs/plans/2026-01-02-database-migration-scripts.md +0 -496
  56. package/docs/plans/2026-01-02-user-login-design.md +0 -37
  57. package/docs/plans/2026-01-02-user-login.md +0 -437
  58. package/docs/plans/2026-01-02-user-registration-design.md +0 -47
  59. package/docs/plans/2026-01-02-user-registration.md +0 -628
  60. package/docs/plans/2026-01-03-roles-permissions-design.md +0 -20
  61. package/docs/plans/2026-01-03-roles-permissions.md +0 -266
  62. package/docs/plans/2026-01-05-authentication-middleware.md +0 -207
  63. package/docs/plans/2026-01-05-member-removal.md +0 -186
  64. package/docs/plans/2026-01-05-organization-creation.md +0 -374
  65. package/docs/plans/2026-01-05-rbac-middleware.md +0 -112
  66. package/docs/plans/2026-01-05-role-configuration.md +0 -441
  67. package/docs/plans/2026-01-06-file-upload-support.md +0 -804
  68. package/docs/plans/2026-01-06-permission-check-hook.md +0 -155
  69. package/docs/plans/2026-01-06-resource-ownership-check.md +0 -231
  70. package/docs/plans/2026-01-07-feedback-tracking-link.md +0 -459
  71. package/docs/plans/2026-01-09-logout-redirect-design.md +0 -52
  72. package/docs/plans/2026-01-09-phase2-3-plan.md +0 -654
  73. package/docs/plans/2026-01-09-portal-execution-plan.md +0 -408
  74. package/docs/plans/2026-01-09-project-delete-feature-design.md +0 -163
  75. package/docs/plans/2026-01-09-project-delete-implementation.md +0 -451
  76. package/docs/plans/2026-01-09-project-edit-delete-design.md +0 -52
  77. package/docs/plans/2026-01-09-settings-center-design.md +0 -114
  78. package/docs/plans/2026-01-09-settings-center.md +0 -948
  79. package/docs/plans/2026-01-10-organization-only-design.md +0 -66
  80. package/docs/plans/2026-01-10-organization-only-implementation.md +0 -433
  81. package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +0 -18
  82. package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +0 -296
  83. package/docs/plans/2026-01-14-e2e-playwright-feedback.md +0 -173
  84. package/docs/plans/2026-01-15-feedback-management-org-context-design.md +0 -82
  85. package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +0 -521
  86. package/docs/plans/2026-01-16-admin-feedback-filters-design.md +0 -75
  87. package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +0 -293
  88. package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +0 -180
  89. package/docs/plans/2026-01-16-e2e-test-fixes.md +0 -158
  90. package/docs/plans/2026-01-17-admin-feedback-filters.md +0 -214
  91. package/docs/plans/2026-01-17-admin-feedback-improvements.md +0 -453
  92. package/docs/plans/2026-01-18-changesets-design.md +0 -40
  93. package/docs/product_changes.md +0 -37
  94. package/docs/project-overview.md +0 -159
  95. package/docs/project-scan-report.json +0 -104
  96. package/docs/route-role-visibility.md +0 -51
  97. package/docs/source-tree-analysis.md +0 -150
  98. package/docs/testing/delete-project-manual-tests.md +0 -18
  99. package/docs/user-story-tracking.md +0 -191
  100. package/eslint.config.mjs +0 -19
  101. package/lib/db/migrations/.gitkeep +0 -0
  102. package/lib/db/migrations/0000_cynical_gladiator.sql +0 -53
  103. package/lib/db/migrations/0001_wandering_sunfire.sql +0 -27
  104. package/lib/db/migrations/0002_shallow_speedball.sql +0 -1
  105. package/lib/db/migrations/0003_add_org_description.sql +0 -1
  106. package/lib/db/migrations/0003_boring_wild_pack.sql +0 -13
  107. package/lib/db/migrations/0004_windy_tyrannus.sql +0 -27
  108. package/lib/db/migrations/0005_perpetual_doorman.sql +0 -5
  109. package/lib/db/migrations/0006_aberrant_captain_midlands.sql +0 -13
  110. package/lib/db/migrations/0007_clever_captain_cross.sql +0 -14
  111. package/lib/db/migrations/0008_sparkling_pandemic.sql +0 -2
  112. package/lib/db/migrations/0009_happy_black_tom.sql +0 -29
  113. package/lib/db/migrations/0010_kind_junta.sql +0 -8
  114. package/lib/db/migrations/0011_mute_squadron_supreme.sql +0 -25
  115. package/lib/db/migrations/0012_giant_power_man.sql +0 -24
  116. package/lib/db/migrations/0013_damp_titanium_man.sql +0 -17
  117. package/lib/db/migrations/0014_blue_alice.sql +0 -18
  118. package/lib/db/migrations/0015_webhook_tables.sql +0 -41
  119. package/lib/db/migrations/0016_github_integration.sql +0 -30
  120. package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +0 -22
  121. package/lib/db/migrations/0017_slimy_inhumans.sql +0 -6
  122. package/lib/db/migrations/0018_same_spitfire.sql +0 -1
  123. package/lib/db/migrations/0019_jittery_loners.sql +0 -16
  124. package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +0 -14
  125. package/lib/db/migrations/meta/0001_snapshot.json +0 -553
  126. package/lib/db/migrations/meta/0002_snapshot.json +0 -560
  127. package/lib/db/migrations/meta/0003_snapshot.json +0 -650
  128. package/lib/db/migrations/meta/0004_snapshot.json +0 -852
  129. package/lib/db/migrations/meta/0005_snapshot.json +0 -900
  130. package/lib/db/migrations/meta/0006_snapshot.json +0 -1011
  131. package/lib/db/migrations/meta/0007_snapshot.json +0 -1125
  132. package/lib/db/migrations/meta/0008_snapshot.json +0 -1146
  133. package/lib/db/migrations/meta/0009_snapshot.json +0 -1386
  134. package/lib/db/migrations/meta/0010_snapshot.json +0 -1419
  135. package/lib/db/migrations/meta/0011_snapshot.json +0 -1615
  136. package/lib/db/migrations/meta/0012_snapshot.json +0 -1805
  137. package/lib/db/migrations/meta/0013_snapshot.json +0 -1948
  138. package/lib/db/migrations/meta/0014_snapshot.json +0 -2082
  139. package/lib/db/migrations/meta/0015_snapshot.json +0 -2476
  140. package/lib/db/migrations/meta/0016_snapshot.json +0 -2633
  141. package/lib/db/migrations/meta/0017_snapshot.json +0 -2680
  142. package/lib/db/migrations/meta/0018_snapshot.json +0 -2686
  143. package/lib/db/migrations/meta/0019_snapshot.json +0 -2741
  144. package/lib/db/schema/projects.ts +0 -145
  145. package/lib/db/schema/user-profiles.ts +0 -31
  146. package/lib/validations/projects.ts +0 -49
  147. package/next-env.d.ts +0 -6
  148. package/playwright.config.ts +0 -44
  149. package/proxy.test.ts +0 -131
  150. package/proxy.ts +0 -116
  151. package/scripts/backup-db.sh +0 -57
  152. package/scripts/backup-db.ts +0 -24
  153. package/scripts/generate-openapi.ts +0 -22
  154. package/scripts/migration-helper.ts +0 -39
  155. package/scripts/pre-deploy.ts +0 -75
  156. package/scripts/restore-db.sh +0 -60
  157. package/scripts/rollback.ts +0 -72
  158. package/scripts/seed-tags.ts +0 -48
  159. package/tests/api/feedback-bulk.test.ts +0 -47
  160. package/tests/api/feedback-by-id.test.ts +0 -67
  161. package/tests/api/feedback-comments-route-import.test.ts +0 -26
  162. package/tests/api/feedback-create.test.ts +0 -71
  163. package/tests/api/feedback-delete.test.ts +0 -160
  164. package/tests/api/feedback-filter.test.ts +0 -250
  165. package/tests/api/feedback-list.test.ts +0 -234
  166. package/tests/api/feedback-route-assignee-condition.test.ts +0 -32
  167. package/tests/api/feedback-similar.test.ts +0 -46
  168. package/tests/api/feedback-sort.test.ts +0 -261
  169. package/tests/api/feedback-status-enum.test.ts +0 -49
  170. package/tests/api/feedback-status-filter.test.ts +0 -117
  171. package/tests/api/feedback-submit-on-behalf.test.ts +0 -269
  172. package/tests/api/feedback.test.ts +0 -175
  173. package/tests/api/identify-jwt.test.ts +0 -25
  174. package/tests/api/invitation-accept.test.ts +0 -213
  175. package/tests/api/organization-invitations.test.ts +0 -186
  176. package/tests/api/organization-members-list.test.ts +0 -79
  177. package/tests/api/organization-members.test.ts +0 -340
  178. package/tests/api/organizations.test.ts +0 -149
  179. package/tests/api/register.test.ts +0 -112
  180. package/tests/api/upload.test.ts +0 -103
  181. package/tests/api/vote.test.ts +0 -82
  182. package/tests/app/admin-feedback-detail-page.test.tsx +0 -25
  183. package/tests/app/admin-feedback-list-page.test.tsx +0 -25
  184. package/tests/app/admin-feedback-new-page.test.tsx +0 -25
  185. package/tests/app/health-route-helpers.test.ts +0 -27
  186. package/tests/app/login-page.test.ts +0 -26
  187. package/tests/app/portal-page.test.ts +0 -29
  188. package/tests/app/project-portal-overview.test.tsx +0 -25
  189. package/tests/app/widget-page-import.test.ts +0 -25
  190. package/tests/components/create-post-dialog-defaults.test.ts +0 -43
  191. package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +0 -27
  192. package/tests/components/feedback/embedded-feedback-form.test.tsx +0 -96
  193. package/tests/components/feedback/feedback-detail.test.tsx +0 -25
  194. package/tests/components/feedback/feedback-stats.test.tsx +0 -49
  195. package/tests/components/feedback-bulk-actions.test.tsx +0 -39
  196. package/tests/components/feedback-i18n-keys.test.ts +0 -70
  197. package/tests/components/feedback-list-controls-compile.test.ts +0 -25
  198. package/tests/components/feedback-list-controls.test.tsx +0 -204
  199. package/tests/components/feedback-list-item.test.tsx +0 -67
  200. package/tests/components/landing/hero.test.tsx +0 -46
  201. package/tests/components/layout/language-switcher.test.tsx +0 -25
  202. package/tests/components/layout/sidebar.test.tsx +0 -157
  203. package/tests/components/login-form.test.ts +0 -25
  204. package/tests/components/organization-form.test.ts +0 -32
  205. package/tests/components/organization-switcher.test.ts +0 -25
  206. package/tests/components/pagination.test.tsx +0 -43
  207. package/tests/components/portal-overview.test.tsx +0 -25
  208. package/tests/components/profile-form.test.tsx +0 -139
  209. package/tests/components/role-selector.test.ts +0 -31
  210. package/tests/components/status-chart.test.tsx +0 -90
  211. package/tests/e2e/auth.e2e.ts +0 -323
  212. package/tests/e2e/feedback-actions.e2e.ts +0 -471
  213. package/tests/e2e/feedback-attachment.e2e.ts +0 -168
  214. package/tests/e2e/feedback-customer.e2e.ts +0 -226
  215. package/tests/e2e/feedback-management.e2e.ts +0 -565
  216. package/tests/e2e/feedback-submit.e2e.ts +0 -133
  217. package/tests/e2e/feedback-view.e2e.ts +0 -297
  218. package/tests/e2e/fixtures/test-data.ts +0 -235
  219. package/tests/e2e/health-check.e2e.ts +0 -230
  220. package/tests/e2e/helpers/test-utils-helpers.test.ts +0 -43
  221. package/tests/e2e/helpers/test-utils.ts +0 -298
  222. package/tests/e2e/integration-placeholders.e2e.ts +0 -199
  223. package/tests/e2e/organization.e2e.ts +0 -292
  224. package/tests/e2e/permissions.e2e.ts +0 -424
  225. package/tests/e2e/project-widget.e2e.ts +0 -63
  226. package/tests/feedback/filters.test.ts +0 -29
  227. package/tests/hooks/use-permissions.test.ts +0 -52
  228. package/tests/lib/ai/classifier.test.ts +0 -104
  229. package/tests/lib/ai/duplicate-detector.test.ts +0 -234
  230. package/tests/lib/attachments-schema.test.ts +0 -30
  231. package/tests/lib/auth/session.test.ts +0 -49
  232. package/tests/lib/auth-client.test.ts +0 -37
  233. package/tests/lib/auth-config.test.ts +0 -26
  234. package/tests/lib/feedback-prefill.test.ts +0 -52
  235. package/tests/lib/feedback-processor.test.ts +0 -41
  236. package/tests/lib/feedback-schema.test.ts +0 -33
  237. package/tests/lib/file-validator.test.ts +0 -48
  238. package/tests/lib/get-feedback-by-id.test.ts +0 -37
  239. package/tests/lib/invitations.test.ts +0 -35
  240. package/tests/lib/login-schema.test.ts +0 -36
  241. package/tests/lib/org-context.test.ts +0 -95
  242. package/tests/lib/organization-access.test.ts +0 -44
  243. package/tests/lib/organization-member-role-schema.test.ts +0 -41
  244. package/tests/lib/permissions.test.ts +0 -88
  245. package/tests/lib/portal-analytics.test.ts +0 -25
  246. package/tests/lib/portal-contributors.test.ts +0 -25
  247. package/tests/lib/portal-copy.test.ts +0 -27
  248. package/tests/lib/portal-i18n.test.ts +0 -30
  249. package/tests/lib/portal-leaderboard-settings.test.ts +0 -25
  250. package/tests/lib/portal-modules.test.ts +0 -25
  251. package/tests/lib/portal-seo.test.ts +0 -25
  252. package/tests/lib/portal-sharing.test.ts +0 -25
  253. package/tests/lib/portal-sorting.test.ts +0 -25
  254. package/tests/lib/portal-theme.test.ts +0 -25
  255. package/tests/lib/rate-limit.test.ts +0 -142
  256. package/tests/lib/resolve-locale.test.ts +0 -34
  257. package/tests/lib/services/backup.test.ts +0 -145
  258. package/tests/lib/user-organizations.test.ts +0 -42
  259. package/tests/lib/user-role-schema.test.ts +0 -33
  260. package/tests/lib/user-schema.test.ts +0 -25
  261. package/tests/setup.ts +0 -74
  262. package/vercel.json +0 -4
@@ -1,521 +0,0 @@
1
- # Feedback Organization Context Implementation Plan
2
-
3
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
-
5
- **Goal:** Enforce organization-aware feedback management across all pages and APIs with a unified org context resolver and a dashboard-only org switcher.
6
-
7
- **Architecture:** Add a server-side org context resolver that reads org ID from URL > header > cookie, validates membership, and is used by all feedback routes. Add a dashboard org switcher that writes org selection to cookie and URL.
8
-
9
- **Tech Stack:** Next.js App Router, React 19, TypeScript, Drizzle ORM, Bun test, Next cookies/headers.
10
-
11
- ---
12
-
13
- ### Task 1: Add organization context resolver + tests
14
-
15
- **Files:**
16
- - Create: `lib/auth/org-context.ts`
17
- - Create: `tests/lib/org-context.test.ts`
18
-
19
- **Step 1: Write the failing test**
20
-
21
- ```ts
22
- import { describe, expect, it } from "bun:test";
23
- import { getOrgContext } from "@/lib/auth/org-context";
24
-
25
- const makeRequest = (options: {
26
- query?: Record<string, string>;
27
- headerOrgId?: string;
28
- cookieOrgId?: string;
29
- }) => {
30
- const url = new URL("https://example.com");
31
- if (options.query) {
32
- for (const [key, value] of Object.entries(options.query)) {
33
- url.searchParams.set(key, value);
34
- }
35
- }
36
-
37
- return {
38
- nextUrl: url,
39
- headers: new Headers(
40
- options.headerOrgId ? { "x-organization-id": options.headerOrgId } : undefined,
41
- ),
42
- cookies: {
43
- get: (name: string) => (name === "orgId" && options.cookieOrgId ? { value: options.cookieOrgId } : undefined),
44
- },
45
- } as const;
46
- };
47
-
48
- const makeDb = (hasMember: boolean) => ({
49
- select: () => ({
50
- from: () => ({
51
- where: () => ({
52
- limit: async () => (hasMember ? [{ role: "admin" }] : []),
53
- }),
54
- }),
55
- }),
56
- });
57
-
58
- describe("getOrgContext", () => {
59
- it("prefers query over header and cookie", async () => {
60
- const req = makeRequest({ query: { organizationId: "org_query" }, headerOrgId: "org_header", cookieOrgId: "org_cookie" });
61
- const context = await getOrgContext({ request: req, db: makeDb(true), userId: "user_1" });
62
- expect(context.organizationId).toBe("org_query");
63
- expect(context.source).toBe("query");
64
- });
65
-
66
- it("throws when membership missing", async () => {
67
- const req = makeRequest({ cookieOrgId: "org_cookie" });
68
- await expect(
69
- getOrgContext({ request: req, db: makeDb(false), userId: "user_1", requireMembership: true })
70
- ).rejects.toThrow("Access denied");
71
- });
72
- });
73
- ```
74
-
75
- **Step 2: Run test to verify it fails**
76
-
77
- Run: `bun test tests/lib/org-context.test.ts`
78
- Expected: FAIL with module not found or missing exports.
79
-
80
- **Step 3: Write minimal implementation**
81
-
82
- ```ts
83
- import { assertOrganizationAccess } from "@/lib/auth/organization";
84
- import type { db as database } from "@/lib/db";
85
-
86
- type Database = NonNullable<typeof database>;
87
-
88
- type OrgContextSource = "query" | "header" | "cookie" | "explicit";
89
-
90
- export type OrgContext = {
91
- organizationId: string;
92
- memberRole: string | null;
93
- source: OrgContextSource;
94
- };
95
-
96
- export async function getOrgContext(options: {
97
- request: { nextUrl: URL; headers: Headers; cookies?: { get: (name: string) => { value: string } | undefined } };
98
- db: Pick<Database, "select">;
99
- userId?: string | null;
100
- organizationId?: string | null;
101
- requireMembership?: boolean;
102
- }): Promise<OrgContext> {
103
- const { request, db, userId, organizationId, requireMembership } = options;
104
- const queryOrgId = request.nextUrl.searchParams.get("organizationId");
105
- const headerOrgId = request.headers.get("x-organization-id");
106
- const cookieOrgId = request.cookies?.get("orgId")?.value ?? null;
107
-
108
- const resolved = organizationId || queryOrgId || headerOrgId || cookieOrgId;
109
- const source: OrgContextSource = organizationId
110
- ? "explicit"
111
- : queryOrgId
112
- ? "query"
113
- : headerOrgId
114
- ? "header"
115
- : "cookie";
116
-
117
- if (!resolved) {
118
- throw new Error("Missing organization");
119
- }
120
-
121
- let memberRole: string | null = null;
122
- if (userId) {
123
- const member = await assertOrganizationAccess(db, userId, resolved);
124
- memberRole = member.role;
125
- } else if (requireMembership) {
126
- throw new Error("Access denied");
127
- }
128
-
129
- return { organizationId: resolved, memberRole, source };
130
- }
131
- ```
132
-
133
- **Step 4: Run test to verify it passes**
134
-
135
- Run: `bun test tests/lib/org-context.test.ts`
136
- Expected: PASS
137
-
138
- **Step 5: Commit**
139
-
140
- ```bash
141
- git add lib/auth/org-context.ts tests/lib/org-context.test.ts
142
- git commit -m "feat: add org context resolver"
143
- ```
144
-
145
- ---
146
-
147
- ### Task 2: Add user organization list helper + tests
148
-
149
- **Files:**
150
- - Modify: `lib/auth/organization.ts`
151
- - Create: `tests/lib/user-organizations.test.ts`
152
-
153
- **Step 1: Write the failing test**
154
-
155
- ```ts
156
- import { describe, expect, it } from "bun:test";
157
- import { getUserOrganizations } from "@/lib/auth/organization";
158
-
159
- const makeDb = (rows: Array<{ id: string; name: string; slug: string; role: string }>) => ({
160
- select: () => ({
161
- from: () => ({
162
- innerJoin: () => ({
163
- where: async () => rows,
164
- }),
165
- }),
166
- }),
167
- });
168
-
169
- describe("getUserOrganizations", () => {
170
- it("returns organization list for user", async () => {
171
- const db = makeDb([{ id: "org_1", name: "Org", slug: "org", role: "admin" }]);
172
- const orgs = await getUserOrganizations(db as never, "user_1");
173
- expect(orgs).toHaveLength(1);
174
- expect(orgs[0].id).toBe("org_1");
175
- });
176
- });
177
- ```
178
-
179
- **Step 2: Run test to verify it fails**
180
-
181
- Run: `bun test tests/lib/user-organizations.test.ts`
182
- Expected: FAIL with missing export.
183
-
184
- **Step 3: Write minimal implementation**
185
-
186
- ```ts
187
- export async function getUserOrganizations(
188
- db: Database,
189
- userId: string
190
- ): Promise<UserOrganization[]> {
191
- return db
192
- .select({
193
- id: organizations.id,
194
- name: organizations.name,
195
- slug: organizations.slug,
196
- role: organizationMembers.role,
197
- })
198
- .from(organizationMembers)
199
- .innerJoin(organizations, eq(organizations.id, organizationMembers.organizationId))
200
- .where(eq(organizationMembers.userId, userId));
201
- }
202
- ```
203
-
204
- **Step 4: Run test to verify it passes**
205
-
206
- Run: `bun test tests/lib/user-organizations.test.ts`
207
- Expected: PASS
208
-
209
- **Step 5: Commit**
210
-
211
- ```bash
212
- git add lib/auth/organization.ts tests/lib/user-organizations.test.ts
213
- git commit -m "feat: add user organization list helper"
214
- ```
215
-
216
- ---
217
-
218
- ### Task 3: Add dashboard organization switcher (client component)
219
-
220
- **Files:**
221
- - Create: `components/dashboard/organization-switcher.tsx`
222
- - Modify: `components/dashboard/index.ts`
223
-
224
- **Step 1: Write the failing test**
225
-
226
- ```ts
227
- import { describe, expect, it } from "bun:test";
228
- import { OrganizationSwitcher } from "@/components/dashboard/organization-switcher";
229
-
230
- describe("OrganizationSwitcher", () => {
231
- it("is defined", () => {
232
- expect(OrganizationSwitcher).toBeDefined();
233
- });
234
- });
235
- ```
236
-
237
- **Step 2: Run test to verify it fails**
238
-
239
- Run: `bun test tests/components/organization-switcher.test.ts`
240
- Expected: FAIL (missing file).
241
-
242
- **Step 3: Write minimal implementation**
243
-
244
- ```tsx
245
- "use client";
246
-
247
- import { useState } from "react";
248
- import { useRouter, useSearchParams } from "next/navigation";
249
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
250
-
251
- type OrgOption = { id: string; name: string; slug: string; role: string };
252
-
253
- type Props = {
254
- organizations: OrgOption[];
255
- currentOrgId: string;
256
- };
257
-
258
- const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
259
-
260
- export function OrganizationSwitcher({ organizations, currentOrgId }: Props) {
261
- const [value, setValue] = useState(currentOrgId);
262
- const router = useRouter();
263
- const searchParams = useSearchParams();
264
-
265
- const handleChange = (orgId: string) => {
266
- setValue(orgId);
267
- const params = new URLSearchParams(searchParams);
268
- params.set("organizationId", orgId);
269
- document.cookie = `orgId=${orgId};path=/;max-age=${COOKIE_MAX_AGE_SECONDS};samesite=lax`;
270
- router.push(`/dashboard?${params.toString()}`);
271
- };
272
-
273
- return (
274
- <Select value={value} onValueChange={handleChange}>
275
- <SelectTrigger className="w-[240px]">
276
- <SelectValue placeholder="选择组织" />
277
- </SelectTrigger>
278
- <SelectContent>
279
- {organizations.map((org) => (
280
- <SelectItem key={org.id} value={org.id}>
281
- {org.name}
282
- </SelectItem>
283
- ))}
284
- </SelectContent>
285
- </Select>
286
- );
287
- }
288
- ```
289
-
290
- **Step 4: Run test to verify it passes**
291
-
292
- Run: `bun test tests/components/organization-switcher.test.ts`
293
- Expected: PASS
294
-
295
- **Step 5: Commit**
296
-
297
- ```bash
298
- git add components/dashboard/organization-switcher.tsx components/dashboard/index.ts tests/components/organization-switcher.test.ts
299
- git commit -m "feat: add dashboard org switcher"
300
- ```
301
-
302
- ---
303
-
304
- ### Task 4: Wire org context into dashboard and feedback pages
305
-
306
- **Files:**
307
- - Modify: `app/(dashboard)/dashboard/page.tsx`
308
- - Modify: `app/(dashboard)/feedback/page.tsx`
309
- - Modify: `app/admin/feedback/page.tsx`
310
- - Modify: `app/admin/feedback/[id]/page.tsx`
311
- - Modify: `app/admin/feedback/[id]/edit/page.tsx`
312
-
313
- **Step 1: Write the failing test**
314
-
315
- ```ts
316
- import { describe, expect, it } from "bun:test";
317
- import { getOrgContext } from "@/lib/auth/org-context";
318
-
319
- describe("dashboard org context", () => {
320
- it("uses cookie org when query missing", async () => {
321
- const request = {
322
- nextUrl: new URL("https://example.com/dashboard"),
323
- headers: new Headers(),
324
- cookies: { get: () => ({ value: "org_cookie" }) },
325
- } as const;
326
- const context = await getOrgContext({ request, db: { select: () => ({}) } as never, userId: null });
327
- expect(context.organizationId).toBe("org_cookie");
328
- });
329
- });
330
- ```
331
-
332
- **Step 2: Run test to verify it fails**
333
-
334
- Run: `bun test tests/lib/org-context.test.ts`
335
- Expected: FAIL until implementations are hooked in.
336
-
337
- **Step 3: Write minimal implementation**
338
-
339
- - In `app/(dashboard)/dashboard/page.tsx`, use `getUserOrganizations` to fetch org list and `getOrgContext` to resolve the active org.
340
- - If no `orgId` cookie exists, default to the first org, set cookie, and use that for stats.
341
- - Render `OrganizationSwitcher` (dashboard only) with org list and active org id.
342
- - In `app/(dashboard)/feedback/page.tsx` and `app/admin/feedback/page.tsx`, remove hardcoded org id and resolve using `getOrgContext` with membership required.
343
-
344
- **Step 4: Run tests to verify pass**
345
-
346
- Run: `bun test tests/lib/org-context.test.ts`
347
- Expected: PASS
348
-
349
- **Step 5: Commit**
350
-
351
- ```bash
352
- git add app/(dashboard)/dashboard/page.tsx app/(dashboard)/feedback/page.tsx app/admin/feedback/page.tsx app/admin/feedback/[id]/page.tsx app/admin/feedback/[id]/edit/page.tsx
353
- git commit -m "feat: use org context in dashboard and admin pages"
354
- ```
355
-
356
- ---
357
-
358
- ### Task 5: Enforce org context in feedback APIs (list and similar)
359
-
360
- **Files:**
361
- - Modify: `app/api/feedback/route.ts`
362
- - Modify: `app/api/feedback/similar/route.ts`
363
-
364
- **Step 1: Write the failing test**
365
-
366
- ```ts
367
- import { describe, expect, it } from "bun:test";
368
- import { getOrgContext } from "@/lib/auth/org-context";
369
-
370
- describe("feedback list API org enforcement", () => {
371
- it("throws when organization is missing", async () => {
372
- const request = { nextUrl: new URL("https://example.com/api/feedback"), headers: new Headers() } as const;
373
- await expect(getOrgContext({ request, db: { select: () => ({}) } as never, userId: "user" })).rejects.toThrow("Missing organization");
374
- });
375
- });
376
- ```
377
-
378
- **Step 2: Run test to verify it fails**
379
-
380
- Run: `bun test tests/lib/org-context.test.ts`
381
- Expected: FAIL until API uses resolver consistently.
382
-
383
- **Step 3: Write minimal implementation**
384
-
385
- - Replace manual `organizationId` resolution with `getOrgContext`.
386
- - Require membership for authenticated routes.
387
- - Use `context.organizationId` in feedback list and similar queries.
388
-
389
- **Step 4: Run tests to verify pass**
390
-
391
- Run: `bun test tests/lib/org-context.test.ts`
392
- Expected: PASS
393
-
394
- **Step 5: Commit**
395
-
396
- ```bash
397
- git add app/api/feedback/route.ts app/api/feedback/similar/route.ts
398
- git commit -m "feat: enforce org context in feedback list APIs"
399
- ```
400
-
401
- ---
402
-
403
- ### Task 6: Enforce org context in feedback detail and child APIs
404
-
405
- **Files:**
406
- - Modify: `app/api/feedback/[id]/route.ts`
407
- - Modify: `app/api/feedback/[id]/comments/route.ts`
408
- - Modify: `app/api/feedback/[id]/vote/route.ts`
409
- - Modify: `app/api/feedback/[id]/duplicates/route.ts`
410
- - Modify: `app/api/feedback/[id]/suggest-tags/route.ts`
411
- - Modify: `app/api/feedback/[id]/processing-status/route.ts`
412
- - Modify: `app/api/feedback/[id]/reclassify/route.ts`
413
-
414
- **Step 1: Write the failing test**
415
-
416
- ```ts
417
- import { describe, expect, it } from "bun:test";
418
- import { getOrgContext } from "@/lib/auth/org-context";
419
-
420
- describe("feedback detail API org enforcement", () => {
421
- it("throws when user is not a member", async () => {
422
- const request = { nextUrl: new URL("https://example.com/api/feedback/1"), headers: new Headers(), cookies: { get: () => ({ value: "org_1" }) } } as const;
423
- const db = { select: () => ({ from: () => ({ where: () => ({ limit: async () => [] }) }) }) };
424
- await expect(getOrgContext({ request, db: db as never, userId: "user", requireMembership: true })).rejects.toThrow("Access denied");
425
- });
426
- });
427
- ```
428
-
429
- **Step 2: Run test to verify it fails**
430
-
431
- Run: `bun test tests/lib/org-context.test.ts`
432
- Expected: FAIL until enforced.
433
-
434
- **Step 3: Write minimal implementation**
435
-
436
- - Resolve org context at the start of each route.
437
- - For id-based operations, verify the feedback’s `organizationId` matches the resolved org before returning or mutating.
438
- - If mismatch, return 404.
439
-
440
- **Step 4: Run tests to verify pass**
441
-
442
- Run: `bun test tests/lib/org-context.test.ts`
443
- Expected: PASS
444
-
445
- **Step 5: Commit**
446
-
447
- ```bash
448
- git add app/api/feedback/[id]/route.ts app/api/feedback/[id]/comments/route.ts app/api/feedback/[id]/vote/route.ts app/api/feedback/[id]/duplicates/route.ts app/api/feedback/[id]/suggest-tags/route.ts app/api/feedback/[id]/processing-status/route.ts app/api/feedback/[id]/reclassify/route.ts
449
- git commit -m "feat: enforce org context in feedback detail APIs"
450
- ```
451
-
452
- ---
453
-
454
- ### Task 7: Update API v1 and public routes for org context consistency
455
-
456
- **Files:**
457
- - Modify: `app/api/v1/feedback/route.ts`
458
- - Modify: `app/api/v1/feedback/[id]/route.ts`
459
- - Modify: `app/[organizationSlug]/page.tsx`
460
- - Modify: `app/[organizationSlug]/feedback/[id]/page.tsx`
461
-
462
- **Step 1: Write the failing test**
463
-
464
- ```ts
465
- import { describe, expect, it } from "bun:test";
466
- import { getOrgContext } from "@/lib/auth/org-context";
467
-
468
- describe("public portal org resolution", () => {
469
- it("accepts explicit organization id", async () => {
470
- const request = { nextUrl: new URL("https://example.com"), headers: new Headers() } as const;
471
- const context = await getOrgContext({ request, db: { select: () => ({}) } as never, organizationId: "org_explicit" });
472
- expect(context.organizationId).toBe("org_explicit");
473
- });
474
- });
475
- ```
476
-
477
- **Step 2: Run test to verify it fails**
478
-
479
- Run: `bun test tests/lib/org-context.test.ts`
480
- Expected: FAIL until explicit org support is used.
481
-
482
- **Step 3: Write minimal implementation**
483
-
484
- - Keep API v1 using API key org id, but pass the resolved org explicitly to the resolver (or bypass if not required).
485
- - Ensure portal routes resolve org via slug and pass explicit org id to avoid cookie contamination.
486
-
487
- **Step 4: Run tests to verify pass**
488
-
489
- Run: `bun test tests/lib/org-context.test.ts`
490
- Expected: PASS
491
-
492
- **Step 5: Commit**
493
-
494
- ```bash
495
- git add app/api/v1/feedback/route.ts app/api/v1/feedback/[id]/route.ts app/[organizationSlug]/page.tsx app/[organizationSlug]/feedback/[id]/page.tsx
496
- git commit -m "feat: align org context for v1 and portal routes"
497
- ```
498
-
499
- ---
500
-
501
- ### Task 8: Final verification and cleanup
502
-
503
- **Files:**
504
- - Modify: `docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md`
505
-
506
- **Step 1: Run unit tests**
507
-
508
- Run: `bun test tests/lib/org-context.test.ts tests/lib/user-organizations.test.ts`
509
- Expected: PASS
510
-
511
- **Step 2: Run lint (acknowledge baseline issues)**
512
-
513
- Run: `bun run lint`
514
- Expected: FAIL due to pre-existing lint errors in `tests/e2e/**`.
515
-
516
- **Step 3: Commit**
517
-
518
- ```bash
519
- git add docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md
520
- git commit -m "chore: record verification steps"
521
- ```
@@ -1,75 +0,0 @@
1
- # Admin Feedback Filters & Sorting Design
2
-
3
- Date: 2026-01-16
4
- Owner: Codex
5
- Scope: /admin/feedback list filters, sorting, pagination, assignee candidates, URL-driven state
6
-
7
- ## Goals
8
- - Provide robust filtering/sorting/pagination for admin feedback list.
9
- - Make list state URL-driven for deep linking and refresh persistence.
10
- - Add assignee, hasVotes, hasReplies filters with multi-select support.
11
- - Add organization members API for assignee candidates and cache client-side.
12
-
13
- ## Non-Goals
14
- - Redesign feedback detail/edit pages.
15
- - Add new permissions or RBAC policies beyond existing org access.
16
- - Introduce new analytics or tracking.
17
-
18
- ## Summary
19
- The admin feedback list becomes fully URL-driven. Filters and sorting are encoded in query parameters, and the list fetches data from `/api/feedback` using those parameters. A new `GET /api/organizations/[orgId]/members` endpoint provides candidate assignees (displayName + userId). The client caches member lists in `sessionStorage` by organization to reduce repeated fetches.
20
-
21
- ## User Experience
22
- - Controls bar above list: search, filters, sorting, primary action (e.g. New Feedback/Export).
23
- - Secondary row: selected filter chips + clear all.
24
- - Right side: total count and per-page size.
25
- - Filters are multi-select for `status`, `type`, `priority`, `assignee`, `hasVotes`, `hasReplies`.
26
- - Any filter/sort change resets `page=1`.
27
-
28
- ## URL Parameters
29
- - `query`: text search across title/description.
30
- - `status`: CSV of statuses.
31
- - `type`: CSV of types.
32
- - `priority`: CSV of priorities.
33
- - `assignee`: CSV of userIds plus `unassigned`.
34
- - `hasVotes`: CSV of boolean values (`true,false`) for multi-select behavior.
35
- - `hasReplies`: CSV of boolean values (`true,false`) for multi-select behavior.
36
- - `sortBy`: `createdAt | voteCount | priority | status`.
37
- - `sortOrder`: `asc | desc`.
38
- - `page`: numeric.
39
- - `pageSize`: numeric.
40
-
41
- Example:
42
- `?status=new,planned&assignee=unassigned,usr_123&hasVotes=true&sortBy=createdAt&sortOrder=desc&page=1`
43
-
44
- ## Data Sources
45
- - Feedback list: existing `/api/feedback` with new query parameters and filters.
46
- - Assignees: new `/api/organizations/[orgId]/members`.
47
-
48
- ## API: GET /api/organizations/[orgId]/members
49
- - Auth: current session user must belong to org.
50
- - Returns array:
51
- - `userId` (string)
52
- - `displayName` (string, derived from user name/email fallback)
53
- - `avatarUrl` (optional)
54
-
55
- ## Assignee Cache
56
- - `sessionStorage` key: `org:${organizationId}:members`.
57
- - Read cache first; if stale or missing, fetch and update cache.
58
- - UI includes fixed option `unassigned` at top.
59
-
60
- ## Error Handling
61
- - Feedback fetch errors render inline error with next step.
62
- - Assignee fetch failures fall back to empty list and show warning UI state.
63
-
64
- ## Accessibility
65
- - Icon-only buttons include `aria-label`.
66
- - Dropdowns and chips are keyboard navigable.
67
- - Loading copy uses ellipsis character `…`.
68
-
69
- ## Testing
70
- - Unit: query param parsing and serialization helpers.
71
- - Integration: list fetch updates on filter change; page resets to 1.
72
- - API: members endpoint returns only org members, denies unauthorized.
73
-
74
- ## Open Questions
75
- - Final CTA label and action target (New Feedback vs Export).