@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,374 +0,0 @@
1
- # Organization Creation Implementation Plan
2
-
3
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
-
5
- **Goal:** Allow an authenticated admin to create a new organization via an API endpoint and a UI form, with unique slug generation and creator assigned as org admin.
6
-
7
- **Architecture:** Add a dedicated API handler + route under `app/api/organizations/` that validates input, generates a unique slug, creates the organization and membership in a transaction, and returns a 201 response. Add a client-side form component rendered on a new dashboard route that posts to the API and redirects to the organization dashboard on success.
8
-
9
- **Tech Stack:** Next.js App Router, React 19, TypeScript, Drizzle ORM, Zod, Bun test runner, Tailwind CSS + shadcn/ui.
10
-
11
- ### Task 1: Organization creation API + tests
12
-
13
- **Files:**
14
- - Create: `app/api/organizations/handler.ts`
15
- - Create: `app/api/organizations/route.ts`
16
- - Create: `lib/validations/organizations.ts`
17
- - Create: `tests/api/organizations.test.ts`
18
-
19
- **Step 1: Write the failing API tests**
20
-
21
- ```ts
22
- import { describe, it, expect } from "bun:test";
23
- import { buildCreateOrganizationHandler } from "@/app/api/organizations/handler";
24
-
25
- type FakeDeps = Parameters<typeof buildCreateOrganizationHandler>[0];
26
-
27
- const makeDeps = () => {
28
- const auth: FakeDeps["auth"] = {
29
- api: {
30
- getSession: async () => ({ user: { id: "user_1" } }),
31
- },
32
- };
33
-
34
- const db: FakeDeps["db"] = {
35
- select: () => ({ from: () => ({ where: () => ({ limit: async () => [] }) }) }),
36
- transaction: async (fn) =>
37
- fn({
38
- insert: () => ({ values: () => ({ returning: async () => [{ id: "org_1", slug: "acme-1234" }] }) }),
39
- }),
40
- };
41
-
42
- return { auth, db } satisfies FakeDeps;
43
- };
44
-
45
- describe("POST /api/organizations", () => {
46
- it("rejects unauthenticated requests", async () => {
47
- const deps = makeDeps();
48
- deps.auth.api.getSession = async () => null;
49
- const handler = buildCreateOrganizationHandler(deps);
50
- const res = await handler(new Request("http://localhost/api/organizations", { method: "POST" }));
51
- expect(res.status).toBe(401);
52
- });
53
-
54
- it("creates organization and admin membership", async () => {
55
- const handler = buildCreateOrganizationHandler(makeDeps());
56
- const res = await handler(
57
- new Request("http://localhost/api/organizations", {
58
- method: "POST",
59
- body: JSON.stringify({ name: "Acme", description: "Test" }),
60
- }),
61
- );
62
- const json = await res.json();
63
- expect(res.status).toBe(201);
64
- expect(json.data.slug).toBeDefined();
65
- });
66
- });
67
- ```
68
-
69
- **Step 2: Run test to verify it fails**
70
-
71
- Run: `bun test tests/api/organizations.test.ts`
72
- Expected: FAIL with "module not found" or "buildCreateOrganizationHandler is not a function"
73
-
74
- **Step 3: Implement validation + handler**
75
-
76
- ```ts
77
- // lib/validations/organizations.ts
78
- import { z } from "zod";
79
-
80
- export const createOrganizationSchema = z.object({
81
- name: z.string().min(1).max(100),
82
- description: z.string().max(500).optional(),
83
- });
84
- ```
85
-
86
- ```ts
87
- // app/api/organizations/handler.ts
88
- import { NextResponse } from "next/server";
89
- import { randomUUID } from "crypto";
90
- import { eq } from "drizzle-orm";
91
- import { createOrganizationSchema } from "@/lib/validations/organizations";
92
- import { organizations, organizationMembers } from "@/lib/db/schema";
93
- import { generateSlug } from "@/lib/utils/slug";
94
- import type { db as database } from "@/lib/db";
95
-
96
- type Database = NonNullable<typeof database>;
97
-
98
- type CreateOrganizationDeps = {
99
- auth: { api: { getSession: (args: { headers: Headers }) => Promise<{ user: { id: string } } | null> } };
100
- db: {
101
- select: Database["select"];
102
- transaction: Database["transaction"];
103
- };
104
- };
105
-
106
- export function buildCreateOrganizationHandler(deps: CreateOrganizationDeps) {
107
- return async function POST(req: Request) {
108
- const session = await deps.auth.api.getSession({ headers: req.headers });
109
- if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
110
-
111
- let body: unknown;
112
- try {
113
- body = await req.json();
114
- } catch {
115
- return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
116
- }
117
-
118
- const parsed = createOrganizationSchema.safeParse(body);
119
- if (!parsed.success) {
120
- return NextResponse.json(
121
- { error: "Invalid request body", details: parsed.error.issues },
122
- { status: 400 },
123
- );
124
- }
125
-
126
- const { name, description } = parsed.data;
127
-
128
- let slug = generateSlug(name);
129
- let counter = 0;
130
- while (true) {
131
- const existing = await deps
132
- .select()
133
- .from(organizations)
134
- .where(eq(organizations.slug, slug))
135
- .limit(1);
136
- if (!existing.length) break;
137
- counter += 1;
138
- slug = `${generateSlug(name)}-${counter}`;
139
- }
140
-
141
- const organizationId = randomUUID();
142
-
143
- const [org] = await deps.db.transaction(async (tx) => {
144
- const [created] = await tx
145
- .insert(organizations)
146
- .values({ id: organizationId, name, slug })
147
- .returning();
148
- await tx.insert(organizationMembers).values({
149
- organizationId,
150
- userId: session.user.id,
151
- role: "admin",
152
- });
153
- return [created];
154
- });
155
-
156
- return NextResponse.json({ data: { ...org, description } }, { status: 201 });
157
- };
158
- }
159
- ```
160
-
161
- **Step 4: Wire the route to handler**
162
-
163
- ```ts
164
- // app/api/organizations/route.ts
165
- import { buildCreateOrganizationHandler } from "./handler";
166
- import { auth } from "@/lib/auth/config";
167
- import { db } from "@/lib/db";
168
-
169
- export const dynamic = "force-dynamic";
170
- export const runtime = "nodejs";
171
-
172
- export const POST = buildCreateOrganizationHandler({ auth, db });
173
- ```
174
-
175
- **Step 5: Run test to verify it passes**
176
-
177
- Run: `bun test tests/api/organizations.test.ts`
178
- Expected: PASS
179
-
180
- **Step 6: Commit**
181
-
182
- ```bash
183
- git add app/api/organizations lib/validations/organizations.ts tests/api/organizations.test.ts
184
- git commit -m "feat: add organization creation api"
185
- ```
186
-
187
- ### Task 2: Organization creation UI + tests
188
-
189
- **Files:**
190
- - Create: `components/settings/organization-form.tsx`
191
- - Create: `app/(dashboard)/settings/organizations/new/page.tsx`
192
- - Create: `tests/components/organization-form.test.ts`
193
-
194
- **Step 1: Write a minimal failing component test**
195
-
196
- ```ts
197
- import { describe, expect, it } from "bun:test";
198
- import { OrganizationForm } from "@/components/settings/organization-form";
199
-
200
- describe("OrganizationForm", () => {
201
- it("is a function", () => {
202
- expect(typeof OrganizationForm).toBe("function");
203
- });
204
- });
205
- ```
206
-
207
- **Step 2: Run test to verify it fails**
208
-
209
- Run: `bun test tests/components/organization-form.test.ts`
210
- Expected: FAIL with "module not found"
211
-
212
- **Step 3: Implement the form component**
213
-
214
- ```tsx
215
- "use client";
216
-
217
- import { useState } from "react";
218
- import { useRouter } from "next/navigation";
219
- import { Button } from "@/components/ui/button";
220
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
221
- import { Input } from "@/components/ui/input";
222
- import { Label } from "@/components/ui/label";
223
- import { Textarea } from "@/components/ui/textarea";
224
-
225
- export function OrganizationForm() {
226
- const router = useRouter();
227
- const [isLoading, setIsLoading] = useState(false);
228
- const [error, setError] = useState<string | null>(null);
229
- const [formData, setFormData] = useState({ name: "", description: "" });
230
-
231
- const handleSubmit = async (e: React.FormEvent) => {
232
- e.preventDefault();
233
- setIsLoading(true);
234
- setError(null);
235
-
236
- const res = await fetch("/api/organizations", {
237
- method: "POST",
238
- headers: { "Content-Type": "application/json" },
239
- body: JSON.stringify(formData),
240
- });
241
-
242
- const json = await res.json();
243
- if (!res.ok) {
244
- setError(json?.error ?? "创建失败,请稍后重试");
245
- setIsLoading(false);
246
- return;
247
- }
248
-
249
- router.push(`/dashboard/organizations/${json.data.slug}`);
250
- };
251
-
252
- return (
253
- <Card>
254
- <CardHeader>
255
- <CardTitle>创建新组织</CardTitle>
256
- <CardDescription>用于管理多个项目或团队</CardDescription>
257
- </CardHeader>
258
- <CardContent>
259
- <form onSubmit={handleSubmit} className="space-y-4">
260
- {error ? (
261
- <div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
262
- {error}
263
- </div>
264
- ) : null}
265
-
266
- <div className="space-y-2">
267
- <Label htmlFor="name">组织名称</Label>
268
- <Input
269
- id="name"
270
- name="name"
271
- value={formData.name}
272
- onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
273
- disabled={isLoading}
274
- required
275
- />
276
- </div>
277
-
278
- <div className="space-y-2">
279
- <Label htmlFor="description">描述</Label>
280
- <Textarea
281
- id="description"
282
- name="description"
283
- value={formData.description}
284
- onChange={(e) =>
285
- setFormData((prev) => ({ ...prev, description: e.target.value }))
286
- }
287
- disabled={isLoading}
288
- rows={4}
289
- />
290
- </div>
291
-
292
- <Button type="submit" className="w-full" disabled={isLoading || !formData.name}>
293
- {isLoading ? "创建中..." : "创建"}
294
- </Button>
295
- </form>
296
- </CardContent>
297
- </Card>
298
- );
299
- }
300
- ```
301
-
302
- **Step 4: Add the page**
303
-
304
- ```tsx
305
- import { OrganizationForm } from "@/components/settings/organization-form";
306
-
307
- export default function NewOrganizationPage() {
308
- return (
309
- <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 px-4 py-12">
310
- <div className="mx-auto flex w-full max-w-2xl flex-col gap-6">
311
- <div>
312
- <h1 className="text-3xl font-semibold tracking-tight text-slate-900">
313
- 创建组织
314
- </h1>
315
- <p className="mt-2 text-sm text-slate-600">
316
- 为新的团队或项目创建独立空间
317
- </p>
318
- </div>
319
- <OrganizationForm />
320
- </div>
321
- </div>
322
- );
323
- }
324
- ```
325
-
326
- **Step 5: Run test to verify it passes**
327
-
328
- Run: `bun test tests/components/organization-form.test.ts`
329
- Expected: PASS
330
-
331
- **Step 6: Commit**
332
-
333
- ```bash
334
- git add app/\(dashboard\)/settings/organizations/new/page.tsx components/settings/organization-form.tsx tests/components/organization-form.test.ts
335
- git commit -m "feat: add organization creation form"
336
- ```
337
-
338
- ### Task 3: Risk checks and validation pass
339
-
340
- **Files:**
341
- - Modify: `tests/api/organizations.test.ts` (if needed for slug collision case)
342
-
343
- **Step 1: Add slug collision test**
344
-
345
- ```ts
346
- it("retries slug generation when collision occurs", async () => {
347
- const deps = makeDeps();
348
- const handler = buildCreateOrganizationHandler(deps);
349
- const res = await handler(
350
- new Request("http://localhost/api/organizations", {
351
- method: "POST",
352
- body: JSON.stringify({ name: "Acme" }),
353
- }),
354
- );
355
- expect(res.status).toBe(201);
356
- });
357
- ```
358
-
359
- **Step 2: Run targeted tests**
360
-
361
- Run: `bun test tests/api/organizations.test.ts`
362
- Expected: PASS
363
-
364
- **Step 3: Run lint for sanity**
365
-
366
- Run: `bun run lint`
367
- Expected: PASS
368
-
369
- **Step 4: Commit**
370
-
371
- ```bash
372
- git add tests/api/organizations.test.ts
373
- git commit -m "test: cover organization slug collisions"
374
- ```
@@ -1,112 +0,0 @@
1
- # RBAC Middleware Implementation Plan
2
-
3
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
-
5
- **Goal:** Add a reusable RBAC middleware helper that enforces permission checks using the existing session and permission system.
6
-
7
- **Architecture:** Implement `requirePermission(permission, req)` in `lib/middleware/rbac.ts` to read the session via `getServerSession(req)`, validate `session.user.role`, and return JSON error responses (401/403) or `NextResponse.next()` on success. Add focused Bun tests in `middleware.test.ts` using the existing session mock.
8
-
9
- **Tech Stack:** Next.js (App Router), TypeScript, Bun test runner, `next/server`.
10
-
11
- ### Task 1: RBAC helper tests
12
-
13
- **Files:**
14
- - Modify: `middleware.test.ts:1-120`
15
-
16
- **Step 1: Write the failing test**
17
-
18
- ```typescript
19
- import { describe, it, expect, mock } from "bun:test";
20
- import { NextRequest } from "next/server";
21
- import { PERMISSIONS } from "@/lib/auth/permissions";
22
- import { requirePermission } from "@/lib/middleware/rbac";
23
-
24
- mock.module("@/lib/auth/session", () => ({
25
- getServerSession: async (req: NextRequest) => {
26
- const isAuthed = req.headers.get("x-test-auth") === "1";
27
- if (!isAuthed) return null;
28
- const role = req.headers.get("x-test-role");
29
- return role ? { user: { id: "u_test", role } } : { user: { id: "u_test" } };
30
- },
31
- }));
32
-
33
- describe("rbac requirePermission", () => {
34
- it("returns 401 when session is missing", async () => {
35
- const req = new NextRequest("http://localhost/api/secure");
36
- const res = await requirePermission(PERMISSIONS.CREATE_FEEDBACK, req);
37
- expect(res.status).toBe(401);
38
- await expect(res.json()).resolves.toEqual({ error: "Unauthorized" });
39
- });
40
-
41
- it("returns 401 when role is missing", async () => {
42
- const req = new NextRequest("http://localhost/api/secure", {
43
- headers: { "x-test-auth": "1" },
44
- });
45
- const res = await requirePermission(PERMISSIONS.CREATE_FEEDBACK, req);
46
- expect(res.status).toBe(401);
47
- await expect(res.json()).resolves.toEqual({ error: "Unauthorized" });
48
- });
49
-
50
- it("returns 403 when role lacks permission", async () => {
51
- const req = new NextRequest("http://localhost/api/secure", {
52
- headers: { "x-test-auth": "1", "x-test-role": "customer" },
53
- });
54
- const res = await requirePermission(PERMISSIONS.MANAGE_ORG, req);
55
- expect(res.status).toBe(403);
56
- await expect(res.json()).resolves.toEqual({ error: "Forbidden" });
57
- });
58
-
59
- it("returns NextResponse.next when permission is allowed", async () => {
60
- const req = new NextRequest("http://localhost/api/secure", {
61
- headers: { "x-test-auth": "1", "x-test-role": "admin" },
62
- });
63
- const res = await requirePermission(PERMISSIONS.MANAGE_ORG, req);
64
- expect(res.status).toBe(200);
65
- });
66
- });
67
- ```
68
-
69
- **Step 2: Run test to verify it fails**
70
-
71
- Run: `bun test middleware.test.ts`
72
- Expected: FAIL with "Cannot find module '@/lib/middleware/rbac'" or failing 401/403 assertions before implementation.
73
-
74
- **Step 3: Write minimal implementation**
75
-
76
- ```typescript
77
- // lib/middleware/rbac.ts
78
- import type { NextRequest } from "next/server";
79
- import { NextResponse } from "next/server";
80
- import { getServerSession } from "@/lib/auth/session";
81
- import { hasPermission, type Permission } from "@/lib/auth/permissions";
82
-
83
- export async function requirePermission(
84
- permission: Permission,
85
- req: NextRequest
86
- ): Promise<NextResponse> {
87
- const session = await getServerSession(req);
88
- const role = session?.user?.role;
89
-
90
- if (!role) {
91
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
92
- }
93
-
94
- if (!hasPermission(role, permission)) {
95
- return NextResponse.json({ error: "Forbidden" }, { status: 403 });
96
- }
97
-
98
- return NextResponse.next();
99
- }
100
- ```
101
-
102
- **Step 4: Run test to verify it passes**
103
-
104
- Run: `bun test middleware.test.ts`
105
- Expected: PASS
106
-
107
- **Step 5: Commit**
108
-
109
- ```bash
110
- git add middleware.test.ts lib/middleware/rbac.ts
111
- git commit -m "feat: add rbac permission middleware helper"
112
- ```