@nexttylabs/echo 0.4.0 → 0.6.0

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