@nexttylabs/echo 0.3.0 → 0.5.0

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