@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.
- package/CHANGELOG.md +25 -0
- package/app/(public)/[organizationSlug]/roadmap/page.tsx +19 -1
- package/app/api/admin/backup/route.ts +22 -4
- package/app/api/auth/register/handler.ts +1 -2
- package/lib/auth/config.ts +0 -7
- package/lib/db/migrations/0000_needy_leech.sql +335 -0
- package/lib/db/migrations/meta/0000_snapshot.json +2186 -1
- package/lib/db/migrations/meta/_journal.json +2 -135
- package/lib/db/schema/auth.ts +0 -1
- package/lib/db/schema/index.ts +0 -1
- package/lib/portal/public-context.tsx +5 -0
- package/package.json +20 -1
- package/.changeset/README.md +0 -21
- package/.changeset/config.json +0 -11
- package/.changeset/cozy-ghosts-care.md +0 -5
- package/.changeset/sharp-lines-stand.md +0 -5
- package/.changeset/sour-doodles-eat.md +0 -5
- package/.changeset/tender-moose-shop.md +0 -5
- package/.github/pull_request_template.md +0 -13
- package/.github/workflows/ci.yml +0 -41
- package/.github/workflows/publish.yml +0 -44
- package/.github/workflows/release.yml +0 -73
- package/AGENTS.md +0 -92
- package/Dockerfile +0 -57
- package/Makefile +0 -77
- package/app/api/internal/domain-lookup/route.ts +0 -67
- package/bun.lock +0 -2503
- package/components/portal/project-switcher.tsx +0 -20
- package/docker-compose.dev.yml +0 -26
- package/docker-compose.yml +0 -98
- package/docs/architecture.md +0 -259
- package/docs/component-inventory.md +0 -261
- package/docs/database-migrations.md +0 -76
- package/docs/development-guide.md +0 -209
- package/docs/e2e-user-flows.csv +0 -31
- package/docs/er-diagram-feedback.mmd +0 -138
- package/docs/er-diagram.mmd +0 -281
- package/docs/i18n-check-report.md +0 -296
- package/docs/index.md +0 -214
- package/docs/logic-chain.md +0 -94
- package/docs/plans/2026-01-02-database-migration-scripts.md +0 -496
- package/docs/plans/2026-01-02-user-login-design.md +0 -37
- package/docs/plans/2026-01-02-user-login.md +0 -437
- package/docs/plans/2026-01-02-user-registration-design.md +0 -47
- package/docs/plans/2026-01-02-user-registration.md +0 -628
- package/docs/plans/2026-01-03-roles-permissions-design.md +0 -20
- package/docs/plans/2026-01-03-roles-permissions.md +0 -266
- package/docs/plans/2026-01-05-authentication-middleware.md +0 -207
- package/docs/plans/2026-01-05-member-removal.md +0 -186
- package/docs/plans/2026-01-05-organization-creation.md +0 -374
- package/docs/plans/2026-01-05-rbac-middleware.md +0 -112
- package/docs/plans/2026-01-05-role-configuration.md +0 -441
- package/docs/plans/2026-01-06-file-upload-support.md +0 -804
- package/docs/plans/2026-01-06-permission-check-hook.md +0 -155
- package/docs/plans/2026-01-06-resource-ownership-check.md +0 -231
- package/docs/plans/2026-01-07-feedback-tracking-link.md +0 -459
- package/docs/plans/2026-01-09-logout-redirect-design.md +0 -52
- package/docs/plans/2026-01-09-phase2-3-plan.md +0 -654
- package/docs/plans/2026-01-09-portal-execution-plan.md +0 -408
- package/docs/plans/2026-01-09-project-delete-feature-design.md +0 -163
- package/docs/plans/2026-01-09-project-delete-implementation.md +0 -451
- package/docs/plans/2026-01-09-project-edit-delete-design.md +0 -52
- package/docs/plans/2026-01-09-settings-center-design.md +0 -114
- package/docs/plans/2026-01-09-settings-center.md +0 -948
- package/docs/plans/2026-01-10-organization-only-design.md +0 -66
- package/docs/plans/2026-01-10-organization-only-implementation.md +0 -433
- package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +0 -18
- package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +0 -296
- package/docs/plans/2026-01-14-e2e-playwright-feedback.md +0 -173
- package/docs/plans/2026-01-15-feedback-management-org-context-design.md +0 -82
- package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +0 -521
- package/docs/plans/2026-01-16-admin-feedback-filters-design.md +0 -75
- package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +0 -293
- package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +0 -180
- package/docs/plans/2026-01-16-e2e-test-fixes.md +0 -158
- package/docs/plans/2026-01-17-admin-feedback-filters.md +0 -214
- package/docs/plans/2026-01-17-admin-feedback-improvements.md +0 -453
- package/docs/plans/2026-01-18-changesets-design.md +0 -40
- package/docs/product_changes.md +0 -37
- package/docs/project-overview.md +0 -159
- package/docs/project-scan-report.json +0 -104
- package/docs/route-role-visibility.md +0 -51
- package/docs/source-tree-analysis.md +0 -150
- package/docs/testing/delete-project-manual-tests.md +0 -18
- package/docs/user-story-tracking.md +0 -191
- package/eslint.config.mjs +0 -19
- package/lib/db/migrations/.gitkeep +0 -0
- package/lib/db/migrations/0000_cynical_gladiator.sql +0 -53
- package/lib/db/migrations/0001_wandering_sunfire.sql +0 -27
- package/lib/db/migrations/0002_shallow_speedball.sql +0 -1
- package/lib/db/migrations/0003_add_org_description.sql +0 -1
- package/lib/db/migrations/0003_boring_wild_pack.sql +0 -13
- package/lib/db/migrations/0004_windy_tyrannus.sql +0 -27
- package/lib/db/migrations/0005_perpetual_doorman.sql +0 -5
- package/lib/db/migrations/0006_aberrant_captain_midlands.sql +0 -13
- package/lib/db/migrations/0007_clever_captain_cross.sql +0 -14
- package/lib/db/migrations/0008_sparkling_pandemic.sql +0 -2
- package/lib/db/migrations/0009_happy_black_tom.sql +0 -29
- package/lib/db/migrations/0010_kind_junta.sql +0 -8
- package/lib/db/migrations/0011_mute_squadron_supreme.sql +0 -25
- package/lib/db/migrations/0012_giant_power_man.sql +0 -24
- package/lib/db/migrations/0013_damp_titanium_man.sql +0 -17
- package/lib/db/migrations/0014_blue_alice.sql +0 -18
- package/lib/db/migrations/0015_webhook_tables.sql +0 -41
- package/lib/db/migrations/0016_github_integration.sql +0 -30
- package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +0 -22
- package/lib/db/migrations/0017_slimy_inhumans.sql +0 -6
- package/lib/db/migrations/0018_same_spitfire.sql +0 -1
- package/lib/db/migrations/0019_jittery_loners.sql +0 -16
- package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +0 -14
- package/lib/db/migrations/meta/0001_snapshot.json +0 -553
- package/lib/db/migrations/meta/0002_snapshot.json +0 -560
- package/lib/db/migrations/meta/0003_snapshot.json +0 -650
- package/lib/db/migrations/meta/0004_snapshot.json +0 -852
- package/lib/db/migrations/meta/0005_snapshot.json +0 -900
- package/lib/db/migrations/meta/0006_snapshot.json +0 -1011
- package/lib/db/migrations/meta/0007_snapshot.json +0 -1125
- package/lib/db/migrations/meta/0008_snapshot.json +0 -1146
- package/lib/db/migrations/meta/0009_snapshot.json +0 -1386
- package/lib/db/migrations/meta/0010_snapshot.json +0 -1419
- package/lib/db/migrations/meta/0011_snapshot.json +0 -1615
- package/lib/db/migrations/meta/0012_snapshot.json +0 -1805
- package/lib/db/migrations/meta/0013_snapshot.json +0 -1948
- package/lib/db/migrations/meta/0014_snapshot.json +0 -2082
- package/lib/db/migrations/meta/0015_snapshot.json +0 -2476
- package/lib/db/migrations/meta/0016_snapshot.json +0 -2633
- package/lib/db/migrations/meta/0017_snapshot.json +0 -2680
- package/lib/db/migrations/meta/0018_snapshot.json +0 -2686
- package/lib/db/migrations/meta/0019_snapshot.json +0 -2741
- package/lib/db/schema/projects.ts +0 -145
- package/lib/db/schema/user-profiles.ts +0 -31
- package/lib/validations/projects.ts +0 -49
- package/next-env.d.ts +0 -6
- package/playwright.config.ts +0 -44
- package/proxy.test.ts +0 -131
- package/proxy.ts +0 -190
- package/scripts/backup-db.sh +0 -57
- package/scripts/backup-db.ts +0 -24
- package/scripts/generate-openapi.ts +0 -22
- package/scripts/migration-helper.ts +0 -39
- package/scripts/pre-deploy.ts +0 -75
- package/scripts/restore-db.sh +0 -60
- package/scripts/rollback.ts +0 -72
- package/scripts/seed-tags.ts +0 -48
- package/tests/api/feedback-bulk.test.ts +0 -47
- package/tests/api/feedback-by-id.test.ts +0 -67
- package/tests/api/feedback-comments-route-import.test.ts +0 -26
- package/tests/api/feedback-create.test.ts +0 -71
- package/tests/api/feedback-delete.test.ts +0 -160
- package/tests/api/feedback-filter.test.ts +0 -250
- package/tests/api/feedback-list.test.ts +0 -234
- package/tests/api/feedback-route-assignee-condition.test.ts +0 -32
- package/tests/api/feedback-similar.test.ts +0 -46
- package/tests/api/feedback-sort.test.ts +0 -261
- package/tests/api/feedback-status-enum.test.ts +0 -49
- package/tests/api/feedback-status-filter.test.ts +0 -117
- package/tests/api/feedback-submit-on-behalf.test.ts +0 -269
- package/tests/api/feedback.test.ts +0 -175
- package/tests/api/identify-jwt.test.ts +0 -25
- package/tests/api/invitation-accept.test.ts +0 -213
- package/tests/api/organization-invitations.test.ts +0 -186
- package/tests/api/organization-members-list.test.ts +0 -79
- package/tests/api/organization-members.test.ts +0 -340
- package/tests/api/organizations.test.ts +0 -149
- package/tests/api/register.test.ts +0 -112
- package/tests/api/upload.test.ts +0 -103
- package/tests/api/vote.test.ts +0 -82
- package/tests/app/admin-feedback-detail-page.test.tsx +0 -25
- package/tests/app/admin-feedback-list-page.test.tsx +0 -25
- package/tests/app/admin-feedback-new-page.test.tsx +0 -25
- package/tests/app/health-route-helpers.test.ts +0 -27
- package/tests/app/login-page.test.ts +0 -26
- package/tests/app/portal-page.test.ts +0 -29
- package/tests/app/project-portal-overview.test.tsx +0 -25
- package/tests/app/widget-page-import.test.ts +0 -25
- package/tests/components/create-post-dialog-defaults.test.ts +0 -43
- package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +0 -27
- package/tests/components/feedback/embedded-feedback-form.test.tsx +0 -96
- package/tests/components/feedback/feedback-detail.test.tsx +0 -25
- package/tests/components/feedback/feedback-stats.test.tsx +0 -49
- package/tests/components/feedback-bulk-actions.test.tsx +0 -39
- package/tests/components/feedback-i18n-keys.test.ts +0 -70
- package/tests/components/feedback-list-controls-compile.test.ts +0 -25
- package/tests/components/feedback-list-controls.test.tsx +0 -204
- package/tests/components/feedback-list-item.test.tsx +0 -67
- package/tests/components/landing/hero.test.tsx +0 -46
- package/tests/components/layout/language-switcher.test.tsx +0 -25
- package/tests/components/layout/sidebar.test.tsx +0 -157
- package/tests/components/login-form.test.ts +0 -25
- package/tests/components/organization-form.test.ts +0 -32
- package/tests/components/organization-switcher.test.ts +0 -25
- package/tests/components/pagination.test.tsx +0 -43
- package/tests/components/portal-overview.test.tsx +0 -25
- package/tests/components/profile-form.test.tsx +0 -139
- package/tests/components/role-selector.test.ts +0 -31
- package/tests/components/status-chart.test.tsx +0 -90
- package/tests/e2e/auth.e2e.ts +0 -323
- package/tests/e2e/feedback-actions.e2e.ts +0 -471
- package/tests/e2e/feedback-attachment.e2e.ts +0 -168
- package/tests/e2e/feedback-customer.e2e.ts +0 -226
- package/tests/e2e/feedback-management.e2e.ts +0 -565
- package/tests/e2e/feedback-submit.e2e.ts +0 -133
- package/tests/e2e/feedback-view.e2e.ts +0 -297
- package/tests/e2e/fixtures/test-data.ts +0 -235
- package/tests/e2e/health-check.e2e.ts +0 -230
- package/tests/e2e/helpers/test-utils-helpers.test.ts +0 -43
- package/tests/e2e/helpers/test-utils.ts +0 -298
- package/tests/e2e/integration-placeholders.e2e.ts +0 -199
- package/tests/e2e/organization.e2e.ts +0 -292
- package/tests/e2e/permissions.e2e.ts +0 -424
- package/tests/e2e/project-widget.e2e.ts +0 -63
- package/tests/feedback/filters.test.ts +0 -29
- package/tests/hooks/use-permissions.test.ts +0 -52
- package/tests/lib/ai/classifier.test.ts +0 -104
- package/tests/lib/ai/duplicate-detector.test.ts +0 -234
- package/tests/lib/attachments-schema.test.ts +0 -30
- package/tests/lib/auth/session.test.ts +0 -49
- package/tests/lib/auth-client.test.ts +0 -37
- package/tests/lib/auth-config.test.ts +0 -26
- package/tests/lib/feedback-prefill.test.ts +0 -52
- package/tests/lib/feedback-processor.test.ts +0 -41
- package/tests/lib/feedback-schema.test.ts +0 -33
- package/tests/lib/file-validator.test.ts +0 -48
- package/tests/lib/get-feedback-by-id.test.ts +0 -37
- package/tests/lib/invitations.test.ts +0 -35
- package/tests/lib/login-schema.test.ts +0 -36
- package/tests/lib/org-context.test.ts +0 -95
- package/tests/lib/organization-access.test.ts +0 -44
- package/tests/lib/organization-member-role-schema.test.ts +0 -41
- package/tests/lib/permissions.test.ts +0 -88
- package/tests/lib/portal-analytics.test.ts +0 -25
- package/tests/lib/portal-contributors.test.ts +0 -25
- package/tests/lib/portal-copy.test.ts +0 -27
- package/tests/lib/portal-i18n.test.ts +0 -30
- package/tests/lib/portal-leaderboard-settings.test.ts +0 -25
- package/tests/lib/portal-modules.test.ts +0 -25
- package/tests/lib/portal-seo.test.ts +0 -25
- package/tests/lib/portal-sharing.test.ts +0 -25
- package/tests/lib/portal-sorting.test.ts +0 -25
- package/tests/lib/portal-theme.test.ts +0 -25
- package/tests/lib/rate-limit.test.ts +0 -142
- package/tests/lib/resolve-locale.test.ts +0 -34
- package/tests/lib/services/backup.test.ts +0 -145
- package/tests/lib/user-organizations.test.ts +0 -42
- package/tests/lib/user-role-schema.test.ts +0 -33
- package/tests/lib/user-schema.test.ts +0 -25
- package/tests/setup.ts +0 -74
- 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
|
-
```
|