@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,441 +0,0 @@
1
- # Role Configuration Implementation Plan
2
-
3
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
-
5
- **Goal:** Allow organization admins to update a member's role via API + UI with validation and immediate permission effect.
6
-
7
- **Architecture:** Add a role schema for org members, implement a PUT handler on the existing members API route with admin/last-admin checks, and add a role selector UI in the members list that calls the API and updates local state.
8
-
9
- **Tech Stack:** Next.js App Router, React 19, TypeScript, Drizzle ORM, Zod, Bun test
10
-
11
- ### Task 1: Add organization member role schema
12
-
13
- **Files:**
14
- - Modify: `lib/validations/organizations.ts`
15
- - Create: `tests/lib/organization-member-role-schema.test.ts`
16
-
17
- **Step 1: Write the failing test**
18
-
19
- ```ts
20
- import { describe, expect, it } from "bun:test";
21
- import { organizationMemberRoleSchema } from "@/lib/validations/organizations";
22
-
23
- describe("organizationMemberRoleSchema", () => {
24
- it("accepts known organization roles", () => {
25
- expect(organizationMemberRoleSchema.safeParse("admin").success).toBe(true);
26
- expect(organizationMemberRoleSchema.safeParse("product_manager").success).toBe(true);
27
- expect(organizationMemberRoleSchema.safeParse("developer").success).toBe(true);
28
- expect(organizationMemberRoleSchema.safeParse("customer_support").success).toBe(true);
29
- });
30
-
31
- it("rejects non-organization roles", () => {
32
- expect(organizationMemberRoleSchema.safeParse("customer").success).toBe(false);
33
- expect(organizationMemberRoleSchema.safeParse("member").success).toBe(false);
34
- });
35
- });
36
- ```
37
-
38
- **Step 2: Run test to verify it fails**
39
-
40
- Run: `bun test tests/lib/organization-member-role-schema.test.ts`
41
- Expected: FAIL with "organizationMemberRoleSchema is not exported"
42
-
43
- **Step 3: Write minimal implementation**
44
-
45
- ```ts
46
- export const organizationMemberRoleSchema = z.enum([
47
- "admin",
48
- "product_manager",
49
- "developer",
50
- "customer_support",
51
- ]);
52
-
53
- export type OrganizationMemberRoleInput = z.infer<typeof organizationMemberRoleSchema>;
54
- ```
55
-
56
- **Step 4: Run test to verify it passes**
57
-
58
- Run: `bun test tests/lib/organization-member-role-schema.test.ts`
59
- Expected: PASS
60
-
61
- **Step 5: Commit**
62
-
63
- ```bash
64
- git add lib/validations/organizations.ts tests/lib/organization-member-role-schema.test.ts
65
- git commit -m "test: add organization member role schema tests"
66
- ```
67
-
68
- ### Task 2: Add PUT role update handler + API tests
69
-
70
- **Files:**
71
- - Modify: `app/api/organizations/[orgId]/members/[memberId]/handler.ts`
72
- - Modify: `app/api/organizations/[orgId]/members/[memberId]/route.ts`
73
- - Modify: `tests/api/organization-members.test.ts`
74
-
75
- **Step 1: Write the failing tests**
76
-
77
- ```ts
78
- import { buildUpdateMemberRoleHandler } from "@/app/api/organizations/[orgId]/members/[memberId]/handler";
79
-
80
- // ...keep existing helpers; add a new helper for update deps
81
-
82
- type UpdateReturn = {
83
- set: () => {
84
- where: () => {
85
- returning: () => Promise<Array<{ userId: string; role: string }>>;
86
- };
87
- };
88
- };
89
-
90
- const makeUpdateDeps = (options: DepsOptions & { updateResultRole?: string }) => {
91
- const base = makeDeps(options);
92
- const updateRole = options.updateResultRole ?? "developer";
93
-
94
- const update = () => ({
95
- set: () => ({
96
- where: () => ({
97
- returning: async () => [{ userId: "user_2", role: updateRole }],
98
- }),
99
- }),
100
- });
101
-
102
- base.db.update = update as unknown as () => UpdateReturn;
103
- return base;
104
- };
105
-
106
- describe("PUT /api/organizations/:orgId/members/:memberId", () => {
107
- it("rejects unauthenticated requests", async () => {
108
- const deps = makeUpdateDeps({});
109
- deps.auth.api.getSession = async () => null;
110
- const handler = buildUpdateMemberRoleHandler(deps);
111
- const res = await handler(
112
- new Request("http://localhost/api/organizations/org_1/members/user_2", {
113
- method: "PUT",
114
- body: JSON.stringify({ role: "developer" }),
115
- }),
116
- { params: { orgId: "org_1", memberId: "user_2" } },
117
- );
118
- expect(res.status).toBe(401);
119
- });
120
-
121
- it("rejects non-admin members", async () => {
122
- const deps = makeUpdateDeps({ requesterRole: "member" });
123
- const handler = buildUpdateMemberRoleHandler(deps);
124
- const res = await handler(
125
- new Request("http://localhost/api/organizations/org_1/members/user_2", {
126
- method: "PUT",
127
- body: JSON.stringify({ role: "developer" }),
128
- }),
129
- { params: { orgId: "org_1", memberId: "user_2" } },
130
- );
131
- expect(res.status).toBe(403);
132
- });
133
-
134
- it("returns 400 for invalid role", async () => {
135
- const deps = makeUpdateDeps({});
136
- const handler = buildUpdateMemberRoleHandler(deps);
137
- const res = await handler(
138
- new Request("http://localhost/api/organizations/org_1/members/user_2", {
139
- method: "PUT",
140
- body: JSON.stringify({ role: "guest" }),
141
- }),
142
- { params: { orgId: "org_1", memberId: "user_2" } },
143
- );
144
- expect(res.status).toBe(400);
145
- });
146
-
147
- it("returns 404 when target member missing", async () => {
148
- const deps = makeUpdateDeps({ targetRole: null });
149
- const handler = buildUpdateMemberRoleHandler(deps);
150
- const res = await handler(
151
- new Request("http://localhost/api/organizations/org_1/members/user_2", {
152
- method: "PUT",
153
- body: JSON.stringify({ role: "developer" }),
154
- }),
155
- { params: { orgId: "org_1", memberId: "user_2" } },
156
- );
157
- expect(res.status).toBe(404);
158
- });
159
-
160
- it("blocks demoting the last admin", async () => {
161
- const deps = makeUpdateDeps({ targetRole: "admin", adminCount: 1 });
162
- const handler = buildUpdateMemberRoleHandler(deps);
163
- const res = await handler(
164
- new Request("http://localhost/api/organizations/org_1/members/user_2", {
165
- method: "PUT",
166
- body: JSON.stringify({ role: "developer" }),
167
- }),
168
- { params: { orgId: "org_1", memberId: "user_2" } },
169
- );
170
- const json = await res.json();
171
- expect(res.status).toBe(400);
172
- expect(json.error).toBe("组织至少需要一个管理员");
173
- });
174
-
175
- it("updates role when allowed", async () => {
176
- const deps = makeUpdateDeps({ targetRole: "developer", adminCount: 2, updateResultRole: "product_manager" });
177
- const handler = buildUpdateMemberRoleHandler(deps);
178
- const res = await handler(
179
- new Request("http://localhost/api/organizations/org_1/members/user_2", {
180
- method: "PUT",
181
- body: JSON.stringify({ role: "product_manager" }),
182
- }),
183
- { params: { orgId: "org_1", memberId: "user_2" } },
184
- );
185
- const json = await res.json();
186
- expect(res.status).toBe(200);
187
- expect(json.data.role).toBe("product_manager");
188
- });
189
- });
190
- ```
191
-
192
- **Step 2: Run test to verify it fails**
193
-
194
- Run: `bun test tests/api/organization-members.test.ts`
195
- Expected: FAIL with "buildUpdateMemberRoleHandler is not exported"
196
-
197
- **Step 3: Write minimal implementation**
198
-
199
- ```ts
200
- import { z } from "zod";
201
- import { organizationMemberRoleSchema } from "@/lib/validations/organizations";
202
-
203
- const updateRoleSchema = z.object({
204
- role: organizationMemberRoleSchema,
205
- });
206
-
207
- export function buildUpdateMemberRoleHandler(deps: RemoveMemberDeps) {
208
- return async function PUT(req: Request, context: RemoveMemberContext) {
209
- const session = await deps.auth.api.getSession({ headers: req.headers });
210
- if (!session) {
211
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
212
- }
213
-
214
- let body: unknown;
215
- try {
216
- body = await req.json();
217
- } catch {
218
- return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
219
- }
220
-
221
- const parsed = updateRoleSchema.safeParse(body);
222
- if (!parsed.success) {
223
- return NextResponse.json(
224
- { error: "Invalid request body", details: parsed.error.issues },
225
- { status: 400 },
226
- );
227
- }
228
-
229
- const { role } = parsed.data;
230
- const { orgId, memberId } = await Promise.resolve(context.params);
231
-
232
- const [requester] = await deps.db
233
- .select()
234
- .from(organizationMembers)
235
- .where(
236
- and(
237
- eq(organizationMembers.organizationId, orgId),
238
- eq(organizationMembers.userId, session.user.id),
239
- ),
240
- )
241
- .limit(1);
242
-
243
- if (!requester || requester.role !== "admin") {
244
- return NextResponse.json({ error: "Forbidden" }, { status: 403 });
245
- }
246
-
247
- const [target] = await deps.db
248
- .select()
249
- .from(organizationMembers)
250
- .where(
251
- and(
252
- eq(organizationMembers.organizationId, orgId),
253
- eq(organizationMembers.userId, memberId),
254
- ),
255
- )
256
- .limit(1);
257
-
258
- if (!target) {
259
- return NextResponse.json({ error: "Member not found" }, { status: 404 });
260
- }
261
-
262
- const [adminCount] = await deps.db
263
- .select({ count: count() })
264
- .from(organizationMembers)
265
- .where(
266
- and(
267
- eq(organizationMembers.organizationId, orgId),
268
- eq(organizationMembers.role, "admin"),
269
- ),
270
- );
271
-
272
- if (target.role === "admin" && role !== "admin" && adminCount.count === 1) {
273
- return NextResponse.json(
274
- { error: "组织至少需要一个管理员" },
275
- { status: 400 },
276
- );
277
- }
278
-
279
- const [updated] = await deps.db
280
- .update(organizationMembers)
281
- .set({ role })
282
- .where(
283
- and(
284
- eq(organizationMembers.organizationId, orgId),
285
- eq(organizationMembers.userId, memberId),
286
- ),
287
- )
288
- .returning();
289
-
290
- return NextResponse.json({ data: updated }, { status: 200 });
291
- };
292
- }
293
- ```
294
-
295
- Update the route to export PUT:
296
-
297
- ```ts
298
- export const PUT = buildUpdateMemberRoleHandler({ auth, db });
299
- ```
300
-
301
- **Step 4: Run test to verify it passes**
302
-
303
- Run: `bun test tests/api/organization-members.test.ts`
304
- Expected: PASS
305
-
306
- **Step 5: Commit**
307
-
308
- ```bash
309
- git add app/api/organizations/[orgId]/members/[memberId]/handler.ts app/api/organizations/[orgId]/members/[memberId]/route.ts tests/api/organization-members.test.ts
310
- git commit -m "feat: add member role update API"
311
- ```
312
-
313
- ### Task 3: Add role selector UI
314
-
315
- **Files:**
316
- - Create: `components/settings/role-selector.tsx`
317
- - Modify: `components/settings/organization-members-list.tsx`
318
- - Create: `tests/components/role-selector.test.ts`
319
-
320
- **Step 1: Write the failing test**
321
-
322
- ```ts
323
- import { describe, expect, it } from "bun:test";
324
- import { ROLE_OPTIONS } from "@/components/settings/role-selector";
325
-
326
- describe("ROLE_OPTIONS", () => {
327
- it("includes all organization roles", () => {
328
- const roles = ROLE_OPTIONS.map((option) => option.value);
329
- expect(roles).toEqual([
330
- "admin",
331
- "product_manager",
332
- "developer",
333
- "customer_support",
334
- ]);
335
- });
336
- });
337
- ```
338
-
339
- **Step 2: Run test to verify it fails**
340
-
341
- Run: `bun test tests/components/role-selector.test.ts`
342
- Expected: FAIL with "ROLE_OPTIONS is not exported"
343
-
344
- **Step 3: Write minimal implementation**
345
-
346
- ```tsx
347
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
348
-
349
- export const ROLE_OPTIONS = [
350
- { value: "admin", label: "管理员" },
351
- { value: "product_manager", label: "产品经理" },
352
- { value: "developer", label: "开发者" },
353
- { value: "customer_support", label: "客服" },
354
- ] as const;
355
-
356
- type RoleSelectorProps = {
357
- value: string;
358
- onChange: (value: string) => void;
359
- disabled?: boolean;
360
- };
361
-
362
- export function RoleSelector({ value, onChange, disabled }: RoleSelectorProps) {
363
- return (
364
- <Select value={value} onValueChange={onChange} disabled={disabled}>
365
- <SelectTrigger className="h-8 w-[140px]">
366
- <SelectValue placeholder="选择角色" />
367
- </SelectTrigger>
368
- <SelectContent>
369
- {ROLE_OPTIONS.map((option) => (
370
- <SelectItem key={option.value} value={option.value}>
371
- {option.label}
372
- </SelectItem>
373
- ))}
374
- </SelectContent>
375
- </Select>
376
- );
377
- }
378
- ```
379
-
380
- Update `OrganizationMembersList` to use the selector and PUT handler:
381
-
382
- ```tsx
383
- const currentUserRole = members.find((member) => member.userId === currentUserId)?.role ?? null;
384
- const canManageRoles = currentUserRole === "admin";
385
-
386
- const handleRoleChange = async (memberId: string, nextRole: string) => {
387
- setError(null);
388
- setPendingMemberId(memberId);
389
-
390
- const res = await fetch(`/api/organizations/${organizationId}/members/${memberId}`, {
391
- method: "PUT",
392
- headers: { "Content-Type": "application/json" },
393
- body: JSON.stringify({ role: nextRole }),
394
- });
395
-
396
- const json = await res.json().catch(() => null);
397
- if (!res.ok) {
398
- setError(json?.error ?? "更新角色失败,请稍后重试");
399
- setPendingMemberId(null);
400
- return;
401
- }
402
-
403
- setMembers((prev) =>
404
- prev.map((member) =>
405
- member.userId === memberId ? { ...member, role: nextRole } : member,
406
- ),
407
- );
408
- setPendingMemberId(null);
409
- };
410
- ```
411
-
412
- Replace the role Badge in the list with `RoleSelector`, disabling it when `!canManageRoles || isPending`.
413
-
414
- **Step 4: Run test to verify it passes**
415
-
416
- Run: `bun test tests/components/role-selector.test.ts`
417
- Expected: PASS
418
-
419
- **Step 5: Commit**
420
-
421
- ```bash
422
- git add components/settings/role-selector.tsx components/settings/organization-members-list.tsx tests/components/role-selector.test.ts
423
- git commit -m "feat: add role selector for organization members"
424
- ```
425
-
426
- ### Task 4: Full test pass
427
-
428
- **Files:**
429
- - N/A
430
-
431
- **Step 1: Run full test suite**
432
-
433
- Run: `bun test`
434
- Expected: PASS
435
-
436
- **Step 2: Commit (if needed)**
437
-
438
- ```bash
439
- git status
440
- # Commit any remaining changes if necessary
441
- ```