@nexttylabs/echo 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/app/(public)/[organizationSlug]/roadmap/page.tsx +19 -1
  3. package/app/api/admin/backup/route.ts +22 -4
  4. package/app/api/auth/register/handler.ts +1 -2
  5. package/lib/auth/config.ts +0 -7
  6. package/lib/db/migrations/0000_needy_leech.sql +335 -0
  7. package/lib/db/migrations/meta/0000_snapshot.json +2186 -1
  8. package/lib/db/migrations/meta/_journal.json +2 -135
  9. package/lib/db/schema/auth.ts +0 -1
  10. package/lib/db/schema/index.ts +0 -1
  11. package/lib/portal/public-context.tsx +5 -0
  12. package/package.json +20 -1
  13. package/.changeset/README.md +0 -21
  14. package/.changeset/config.json +0 -11
  15. package/.changeset/cozy-ghosts-care.md +0 -5
  16. package/.changeset/sharp-lines-stand.md +0 -5
  17. package/.changeset/sour-doodles-eat.md +0 -5
  18. package/.changeset/tender-moose-shop.md +0 -5
  19. package/.github/pull_request_template.md +0 -13
  20. package/.github/workflows/ci.yml +0 -41
  21. package/.github/workflows/publish.yml +0 -44
  22. package/.github/workflows/release.yml +0 -73
  23. package/AGENTS.md +0 -92
  24. package/Dockerfile +0 -57
  25. package/Makefile +0 -77
  26. package/bun.lock +0 -2503
  27. package/components/portal/project-switcher.tsx +0 -20
  28. package/docker-compose.dev.yml +0 -26
  29. package/docker-compose.yml +0 -98
  30. package/docs/architecture.md +0 -259
  31. package/docs/component-inventory.md +0 -261
  32. package/docs/database-migrations.md +0 -76
  33. package/docs/development-guide.md +0 -209
  34. package/docs/e2e-user-flows.csv +0 -31
  35. package/docs/er-diagram-feedback.mmd +0 -138
  36. package/docs/er-diagram.mmd +0 -281
  37. package/docs/i18n-check-report.md +0 -296
  38. package/docs/index.md +0 -214
  39. package/docs/logic-chain.md +0 -94
  40. package/docs/plans/2026-01-02-database-migration-scripts.md +0 -496
  41. package/docs/plans/2026-01-02-user-login-design.md +0 -37
  42. package/docs/plans/2026-01-02-user-login.md +0 -437
  43. package/docs/plans/2026-01-02-user-registration-design.md +0 -47
  44. package/docs/plans/2026-01-02-user-registration.md +0 -628
  45. package/docs/plans/2026-01-03-roles-permissions-design.md +0 -20
  46. package/docs/plans/2026-01-03-roles-permissions.md +0 -266
  47. package/docs/plans/2026-01-05-authentication-middleware.md +0 -207
  48. package/docs/plans/2026-01-05-member-removal.md +0 -186
  49. package/docs/plans/2026-01-05-organization-creation.md +0 -374
  50. package/docs/plans/2026-01-05-rbac-middleware.md +0 -112
  51. package/docs/plans/2026-01-05-role-configuration.md +0 -441
  52. package/docs/plans/2026-01-06-file-upload-support.md +0 -804
  53. package/docs/plans/2026-01-06-permission-check-hook.md +0 -155
  54. package/docs/plans/2026-01-06-resource-ownership-check.md +0 -231
  55. package/docs/plans/2026-01-07-feedback-tracking-link.md +0 -459
  56. package/docs/plans/2026-01-09-logout-redirect-design.md +0 -52
  57. package/docs/plans/2026-01-09-phase2-3-plan.md +0 -654
  58. package/docs/plans/2026-01-09-portal-execution-plan.md +0 -408
  59. package/docs/plans/2026-01-09-project-delete-feature-design.md +0 -163
  60. package/docs/plans/2026-01-09-project-delete-implementation.md +0 -451
  61. package/docs/plans/2026-01-09-project-edit-delete-design.md +0 -52
  62. package/docs/plans/2026-01-09-settings-center-design.md +0 -114
  63. package/docs/plans/2026-01-09-settings-center.md +0 -948
  64. package/docs/plans/2026-01-10-organization-only-design.md +0 -66
  65. package/docs/plans/2026-01-10-organization-only-implementation.md +0 -433
  66. package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +0 -18
  67. package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +0 -296
  68. package/docs/plans/2026-01-14-e2e-playwright-feedback.md +0 -173
  69. package/docs/plans/2026-01-15-feedback-management-org-context-design.md +0 -82
  70. package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +0 -521
  71. package/docs/plans/2026-01-16-admin-feedback-filters-design.md +0 -75
  72. package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +0 -293
  73. package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +0 -180
  74. package/docs/plans/2026-01-16-e2e-test-fixes.md +0 -158
  75. package/docs/plans/2026-01-17-admin-feedback-filters.md +0 -214
  76. package/docs/plans/2026-01-17-admin-feedback-improvements.md +0 -453
  77. package/docs/plans/2026-01-18-changesets-design.md +0 -40
  78. package/docs/product_changes.md +0 -37
  79. package/docs/project-overview.md +0 -159
  80. package/docs/project-scan-report.json +0 -104
  81. package/docs/route-role-visibility.md +0 -51
  82. package/docs/source-tree-analysis.md +0 -150
  83. package/docs/testing/delete-project-manual-tests.md +0 -18
  84. package/docs/user-story-tracking.md +0 -191
  85. package/eslint.config.mjs +0 -19
  86. package/lib/db/migrations/.gitkeep +0 -0
  87. package/lib/db/migrations/0000_cynical_gladiator.sql +0 -53
  88. package/lib/db/migrations/0001_wandering_sunfire.sql +0 -27
  89. package/lib/db/migrations/0002_shallow_speedball.sql +0 -1
  90. package/lib/db/migrations/0003_add_org_description.sql +0 -1
  91. package/lib/db/migrations/0003_boring_wild_pack.sql +0 -13
  92. package/lib/db/migrations/0004_windy_tyrannus.sql +0 -27
  93. package/lib/db/migrations/0005_perpetual_doorman.sql +0 -5
  94. package/lib/db/migrations/0006_aberrant_captain_midlands.sql +0 -13
  95. package/lib/db/migrations/0007_clever_captain_cross.sql +0 -14
  96. package/lib/db/migrations/0008_sparkling_pandemic.sql +0 -2
  97. package/lib/db/migrations/0009_happy_black_tom.sql +0 -29
  98. package/lib/db/migrations/0010_kind_junta.sql +0 -8
  99. package/lib/db/migrations/0011_mute_squadron_supreme.sql +0 -25
  100. package/lib/db/migrations/0012_giant_power_man.sql +0 -24
  101. package/lib/db/migrations/0013_damp_titanium_man.sql +0 -17
  102. package/lib/db/migrations/0014_blue_alice.sql +0 -18
  103. package/lib/db/migrations/0015_webhook_tables.sql +0 -41
  104. package/lib/db/migrations/0016_github_integration.sql +0 -30
  105. package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +0 -22
  106. package/lib/db/migrations/0017_slimy_inhumans.sql +0 -6
  107. package/lib/db/migrations/0018_same_spitfire.sql +0 -1
  108. package/lib/db/migrations/0019_jittery_loners.sql +0 -16
  109. package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +0 -14
  110. package/lib/db/migrations/meta/0001_snapshot.json +0 -553
  111. package/lib/db/migrations/meta/0002_snapshot.json +0 -560
  112. package/lib/db/migrations/meta/0003_snapshot.json +0 -650
  113. package/lib/db/migrations/meta/0004_snapshot.json +0 -852
  114. package/lib/db/migrations/meta/0005_snapshot.json +0 -900
  115. package/lib/db/migrations/meta/0006_snapshot.json +0 -1011
  116. package/lib/db/migrations/meta/0007_snapshot.json +0 -1125
  117. package/lib/db/migrations/meta/0008_snapshot.json +0 -1146
  118. package/lib/db/migrations/meta/0009_snapshot.json +0 -1386
  119. package/lib/db/migrations/meta/0010_snapshot.json +0 -1419
  120. package/lib/db/migrations/meta/0011_snapshot.json +0 -1615
  121. package/lib/db/migrations/meta/0012_snapshot.json +0 -1805
  122. package/lib/db/migrations/meta/0013_snapshot.json +0 -1948
  123. package/lib/db/migrations/meta/0014_snapshot.json +0 -2082
  124. package/lib/db/migrations/meta/0015_snapshot.json +0 -2476
  125. package/lib/db/migrations/meta/0016_snapshot.json +0 -2633
  126. package/lib/db/migrations/meta/0017_snapshot.json +0 -2680
  127. package/lib/db/migrations/meta/0018_snapshot.json +0 -2686
  128. package/lib/db/migrations/meta/0019_snapshot.json +0 -2741
  129. package/lib/db/schema/projects.ts +0 -145
  130. package/lib/db/schema/user-profiles.ts +0 -31
  131. package/lib/validations/projects.ts +0 -49
  132. package/next-env.d.ts +0 -6
  133. package/playwright.config.ts +0 -44
  134. package/proxy.test.ts +0 -131
  135. package/proxy.ts +0 -116
  136. package/scripts/backup-db.sh +0 -57
  137. package/scripts/backup-db.ts +0 -24
  138. package/scripts/generate-openapi.ts +0 -22
  139. package/scripts/migration-helper.ts +0 -39
  140. package/scripts/pre-deploy.ts +0 -75
  141. package/scripts/restore-db.sh +0 -60
  142. package/scripts/rollback.ts +0 -72
  143. package/scripts/seed-tags.ts +0 -48
  144. package/tests/api/feedback-bulk.test.ts +0 -47
  145. package/tests/api/feedback-by-id.test.ts +0 -67
  146. package/tests/api/feedback-comments-route-import.test.ts +0 -26
  147. package/tests/api/feedback-create.test.ts +0 -71
  148. package/tests/api/feedback-delete.test.ts +0 -160
  149. package/tests/api/feedback-filter.test.ts +0 -250
  150. package/tests/api/feedback-list.test.ts +0 -234
  151. package/tests/api/feedback-route-assignee-condition.test.ts +0 -32
  152. package/tests/api/feedback-similar.test.ts +0 -46
  153. package/tests/api/feedback-sort.test.ts +0 -261
  154. package/tests/api/feedback-status-enum.test.ts +0 -49
  155. package/tests/api/feedback-status-filter.test.ts +0 -117
  156. package/tests/api/feedback-submit-on-behalf.test.ts +0 -269
  157. package/tests/api/feedback.test.ts +0 -175
  158. package/tests/api/identify-jwt.test.ts +0 -25
  159. package/tests/api/invitation-accept.test.ts +0 -213
  160. package/tests/api/organization-invitations.test.ts +0 -186
  161. package/tests/api/organization-members-list.test.ts +0 -79
  162. package/tests/api/organization-members.test.ts +0 -340
  163. package/tests/api/organizations.test.ts +0 -149
  164. package/tests/api/register.test.ts +0 -112
  165. package/tests/api/upload.test.ts +0 -103
  166. package/tests/api/vote.test.ts +0 -82
  167. package/tests/app/admin-feedback-detail-page.test.tsx +0 -25
  168. package/tests/app/admin-feedback-list-page.test.tsx +0 -25
  169. package/tests/app/admin-feedback-new-page.test.tsx +0 -25
  170. package/tests/app/health-route-helpers.test.ts +0 -27
  171. package/tests/app/login-page.test.ts +0 -26
  172. package/tests/app/portal-page.test.ts +0 -29
  173. package/tests/app/project-portal-overview.test.tsx +0 -25
  174. package/tests/app/widget-page-import.test.ts +0 -25
  175. package/tests/components/create-post-dialog-defaults.test.ts +0 -43
  176. package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +0 -27
  177. package/tests/components/feedback/embedded-feedback-form.test.tsx +0 -96
  178. package/tests/components/feedback/feedback-detail.test.tsx +0 -25
  179. package/tests/components/feedback/feedback-stats.test.tsx +0 -49
  180. package/tests/components/feedback-bulk-actions.test.tsx +0 -39
  181. package/tests/components/feedback-i18n-keys.test.ts +0 -70
  182. package/tests/components/feedback-list-controls-compile.test.ts +0 -25
  183. package/tests/components/feedback-list-controls.test.tsx +0 -204
  184. package/tests/components/feedback-list-item.test.tsx +0 -67
  185. package/tests/components/landing/hero.test.tsx +0 -46
  186. package/tests/components/layout/language-switcher.test.tsx +0 -25
  187. package/tests/components/layout/sidebar.test.tsx +0 -157
  188. package/tests/components/login-form.test.ts +0 -25
  189. package/tests/components/organization-form.test.ts +0 -32
  190. package/tests/components/organization-switcher.test.ts +0 -25
  191. package/tests/components/pagination.test.tsx +0 -43
  192. package/tests/components/portal-overview.test.tsx +0 -25
  193. package/tests/components/profile-form.test.tsx +0 -139
  194. package/tests/components/role-selector.test.ts +0 -31
  195. package/tests/components/status-chart.test.tsx +0 -90
  196. package/tests/e2e/auth.e2e.ts +0 -323
  197. package/tests/e2e/feedback-actions.e2e.ts +0 -471
  198. package/tests/e2e/feedback-attachment.e2e.ts +0 -168
  199. package/tests/e2e/feedback-customer.e2e.ts +0 -226
  200. package/tests/e2e/feedback-management.e2e.ts +0 -565
  201. package/tests/e2e/feedback-submit.e2e.ts +0 -133
  202. package/tests/e2e/feedback-view.e2e.ts +0 -297
  203. package/tests/e2e/fixtures/test-data.ts +0 -235
  204. package/tests/e2e/health-check.e2e.ts +0 -230
  205. package/tests/e2e/helpers/test-utils-helpers.test.ts +0 -43
  206. package/tests/e2e/helpers/test-utils.ts +0 -298
  207. package/tests/e2e/integration-placeholders.e2e.ts +0 -199
  208. package/tests/e2e/organization.e2e.ts +0 -292
  209. package/tests/e2e/permissions.e2e.ts +0 -424
  210. package/tests/e2e/project-widget.e2e.ts +0 -63
  211. package/tests/feedback/filters.test.ts +0 -29
  212. package/tests/hooks/use-permissions.test.ts +0 -52
  213. package/tests/lib/ai/classifier.test.ts +0 -104
  214. package/tests/lib/ai/duplicate-detector.test.ts +0 -234
  215. package/tests/lib/attachments-schema.test.ts +0 -30
  216. package/tests/lib/auth/session.test.ts +0 -49
  217. package/tests/lib/auth-client.test.ts +0 -37
  218. package/tests/lib/auth-config.test.ts +0 -26
  219. package/tests/lib/feedback-prefill.test.ts +0 -52
  220. package/tests/lib/feedback-processor.test.ts +0 -41
  221. package/tests/lib/feedback-schema.test.ts +0 -33
  222. package/tests/lib/file-validator.test.ts +0 -48
  223. package/tests/lib/get-feedback-by-id.test.ts +0 -37
  224. package/tests/lib/invitations.test.ts +0 -35
  225. package/tests/lib/login-schema.test.ts +0 -36
  226. package/tests/lib/org-context.test.ts +0 -95
  227. package/tests/lib/organization-access.test.ts +0 -44
  228. package/tests/lib/organization-member-role-schema.test.ts +0 -41
  229. package/tests/lib/permissions.test.ts +0 -88
  230. package/tests/lib/portal-analytics.test.ts +0 -25
  231. package/tests/lib/portal-contributors.test.ts +0 -25
  232. package/tests/lib/portal-copy.test.ts +0 -27
  233. package/tests/lib/portal-i18n.test.ts +0 -30
  234. package/tests/lib/portal-leaderboard-settings.test.ts +0 -25
  235. package/tests/lib/portal-modules.test.ts +0 -25
  236. package/tests/lib/portal-seo.test.ts +0 -25
  237. package/tests/lib/portal-sharing.test.ts +0 -25
  238. package/tests/lib/portal-sorting.test.ts +0 -25
  239. package/tests/lib/portal-theme.test.ts +0 -25
  240. package/tests/lib/rate-limit.test.ts +0 -142
  241. package/tests/lib/resolve-locale.test.ts +0 -34
  242. package/tests/lib/services/backup.test.ts +0 -145
  243. package/tests/lib/user-organizations.test.ts +0 -42
  244. package/tests/lib/user-role-schema.test.ts +0 -33
  245. package/tests/lib/user-schema.test.ts +0 -25
  246. package/tests/setup.ts +0 -74
  247. package/vercel.json +0 -4
@@ -1,628 +0,0 @@
1
- # User Registration Implementation Plan
2
-
3
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
-
5
- **Goal:** Implement user registration with Better Auth (email/password), create a profile + default organization, and provide a register UI.
6
-
7
- **Architecture:** Better Auth owns auth tables and session cookies. Business data (profiles/organizations/members) lives in our own tables linked by userId. A custom `/api/auth/register` route validates input, calls `auth.api.signUpEmail`, then creates profile/org/member records.
8
-
9
- **Tech Stack:** Next.js App Router, Better Auth, Drizzle ORM, Bun, Zod, Tailwind, shadcn/ui.
10
-
11
- **Skills:** @superpowers:test-driven-development, @senior-frontend, @ui-styling
12
-
13
- ---
14
-
15
- ### Task 1: Bootstrap Better Auth + schema generation
16
-
17
- **Files:**
18
- - Modify: `package.json`
19
- - Create: `lib/auth/config.ts`
20
- - Create: `app/api/auth/[...all]/route.ts`
21
- - Create (generated): `lib/db/schema/auth.ts`
22
- - Modify: `lib/db/schema/index.ts`
23
-
24
- **Step 1: Add dependencies**
25
-
26
- Run:
27
- ```bash
28
- bun add better-auth zod
29
- ```
30
- Expected: deps added in `package.json` and lockfile.
31
-
32
- **Step 2: Create Better Auth config**
33
-
34
- Create `lib/auth/config.ts` (use relative imports so the Better Auth CLI can resolve modules):
35
- ```ts
36
- import { betterAuth } from "better-auth";
37
- import { drizzleAdapter } from "better-auth/adapters/drizzle";
38
- import { nextCookies } from "better-auth/next-js";
39
- import { db } from "../db";
40
-
41
- if (!db) {
42
- throw new Error("DATABASE_URL is not configured");
43
- }
44
-
45
- export const auth = betterAuth({
46
- database: drizzleAdapter(db, {
47
- provider: "pg",
48
- }),
49
- emailAndPassword: {
50
- enabled: true,
51
- },
52
- plugins: [nextCookies()], // keep this last
53
- });
54
- ```
55
-
56
- **Step 3: Mount Better Auth handler**
57
-
58
- Create `app/api/auth/[...all]/route.ts`:
59
- ```ts
60
- import { auth } from "@/lib/auth/config";
61
- import { toNextJsHandler } from "better-auth/next-js";
62
-
63
- export const { GET, POST } = toNextJsHandler(auth);
64
- ```
65
-
66
- **Step 4: Generate Better Auth Drizzle schema**
67
-
68
- Run:
69
- ```bash
70
- bunx @better-auth/cli@latest generate --config lib/auth/config.ts --output lib/db/schema/auth.ts
71
- ```
72
- Expected: a new Drizzle schema file defining Better Auth tables.
73
-
74
- **Step 5: Export schema for drizzle-kit**
75
-
76
- Update `lib/db/schema/index.ts`:
77
- ```ts
78
- export * from "./auth";
79
- ```
80
-
81
- **Step 6: Generate migration (no apply yet)**
82
-
83
- Run:
84
- ```bash
85
- bun run db:generate
86
- ```
87
- Expected: new SQL in `lib/db/migrations/`.
88
-
89
- **Step 7: Commit**
90
-
91
- ```bash
92
- git add package.json bun.lockb lib/auth/config.ts app/api/auth/[...all]/route.ts lib/db/schema/auth.ts lib/db/schema/index.ts lib/db/migrations
93
- git commit -m "feat: add better-auth config and schema"
94
- ```
95
-
96
- ---
97
-
98
- ### Task 2: Business domain schemas + slug helper
99
-
100
- **Files:**
101
- - Create: `lib/db/schema/user-profiles.ts`
102
- - Create: `lib/db/schema/organizations.ts`
103
- - Create: `lib/db/schema/organization-members.ts`
104
- - Modify: `lib/db/schema/index.ts`
105
- - Create: `lib/utils/slug.ts`
106
-
107
- **Step 1: Add schemas**
108
-
109
- Create `lib/db/schema/user-profiles.ts` (adjust `user` import based on generated auth schema name):
110
- ```ts
111
- import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
112
- import { user } from "./auth"; // update if generated export name differs
113
-
114
- export const userProfiles = pgTable("user_profiles", {
115
- userId: text("user_id").primaryKey().references(() => user.id, { onDelete: "cascade" }),
116
- name: text("name").notNull(),
117
- createdAt: timestamp("created_at").defaultNow().notNull(),
118
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
119
- });
120
- ```
121
-
122
- Create `lib/db/schema/organizations.ts`:
123
- ```ts
124
- import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
125
-
126
- export const organizations = pgTable("organizations", {
127
- id: text("id").primaryKey(),
128
- name: text("name").notNull(),
129
- slug: text("slug").notNull().unique(),
130
- createdAt: timestamp("created_at").defaultNow().notNull(),
131
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
132
- });
133
- ```
134
-
135
- Create `lib/db/schema/organization-members.ts`:
136
- ```ts
137
- import { pgTable, primaryKey, text, timestamp } from "drizzle-orm/pg-core";
138
- import { organizations } from "./organizations";
139
- import { user } from "./auth"; // update if generated export name differs
140
-
141
- export const organizationMembers = pgTable(
142
- "organization_members",
143
- {
144
- organizationId: text("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
145
- userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
146
- role: text("role").notNull(),
147
- createdAt: timestamp("created_at").defaultNow().notNull(),
148
- },
149
- (t) => ({
150
- pk: primaryKey({ columns: [t.organizationId, t.userId] }),
151
- })
152
- );
153
- ```
154
-
155
- Update `lib/db/schema/index.ts`:
156
- ```ts
157
- export * from "./auth";
158
- export * from "./user-profiles";
159
- export * from "./organizations";
160
- export * from "./organization-members";
161
- ```
162
-
163
- Create `lib/utils/slug.ts`:
164
- ```ts
165
- export function generateSlug(name: string): string {
166
- const base = name
167
- .toLowerCase()
168
- .trim()
169
- .replace(/\s+/g, "-")
170
- .replace(/[^\w-]/g, "")
171
- .replace(/--+/g, "-");
172
-
173
- const suffix = Math.random().toString(36).slice(2, 6);
174
- return `${base}-${suffix}`;
175
- }
176
- ```
177
-
178
- **Step 2: Generate migration**
179
-
180
- Run:
181
- ```bash
182
- bun run db:generate
183
- ```
184
- Expected: migration adds business tables.
185
-
186
- **Step 3: Commit**
187
-
188
- ```bash
189
- git add lib/db/schema lib/utils/slug.ts lib/db/migrations
190
- git commit -m "feat: add profile and organization schemas"
191
- ```
192
-
193
- ---
194
-
195
- ### Task 3: Registration API (TDD)
196
-
197
- **Files:**
198
- - Create: `lib/validations/auth.ts`
199
- - Create: `app/api/auth/register/handler.ts`
200
- - Create: `app/api/auth/register/route.ts`
201
- - Test: `tests/api/register.test.ts`
202
-
203
- **Step 1: Write failing tests**
204
-
205
- Create `tests/api/register.test.ts`:
206
- ```ts
207
- import { describe, it, expect } from "bun:test";
208
- import { buildRegisterHandler } from "@/app/api/auth/register/handler";
209
- import { APIError } from "better-auth/api";
210
-
211
- const makeDeps = () => {
212
- const cookiesHeader = "session=token; Path=/; HttpOnly";
213
-
214
- const auth = {
215
- api: {
216
- signUpEmail: async () => ({
217
- headers: new Headers({ "set-cookie": cookiesHeader }),
218
- response: new Response(JSON.stringify({
219
- user: { id: "user_1", email: "john@example.com" },
220
- }))
221
- })
222
- }
223
- };
224
-
225
- const db = {
226
- transaction: async (fn: (tx: any) => Promise<void>) => fn({
227
- insert: () => ({ values: async () => {} })
228
- })
229
- };
230
-
231
- return { auth, db };
232
- };
233
-
234
- describe("POST /api/auth/register", () => {
235
- it("registers a user and sets cookie", async () => {
236
- const handler = buildRegisterHandler(makeDeps());
237
- const req = new Request("http://localhost/api/auth/register", {
238
- method: "POST",
239
- body: JSON.stringify({ name: "John", email: "john@example.com", password: "Password123" })
240
- });
241
-
242
- const res = await handler(req);
243
- const json = await res.json();
244
-
245
- expect(res.status).toBe(201);
246
- expect(json.data.user.email).toBe("john@example.com");
247
- expect(res.headers.get("set-cookie")).toContain("session=");
248
- });
249
-
250
- it("returns 409 when email exists", async () => {
251
- const deps = makeDeps();
252
- deps.auth.api.signUpEmail = async () => {
253
- throw new APIError("Email exists", { status: 409 });
254
- };
255
-
256
- const handler = buildRegisterHandler(deps);
257
- const req = new Request("http://localhost/api/auth/register", {
258
- method: "POST",
259
- body: JSON.stringify({ name: "John", email: "john@example.com", password: "Password123" })
260
- });
261
-
262
- const res = await handler(req);
263
- const json = await res.json();
264
-
265
- expect(res.status).toBe(409);
266
- expect(json.code).toBe("EMAIL_EXISTS");
267
- });
268
-
269
- it("validates email and password", async () => {
270
- const handler = buildRegisterHandler(makeDeps());
271
- const req = new Request("http://localhost/api/auth/register", {
272
- method: "POST",
273
- body: JSON.stringify({ name: "John", email: "bad-email", password: "weak" })
274
- });
275
-
276
- const res = await handler(req);
277
- const json = await res.json();
278
-
279
- expect(res.status).toBe(400);
280
- expect(json.code).toBe("VALIDATION_ERROR");
281
- });
282
- });
283
- ```
284
-
285
- **Step 2: Run test to verify it fails**
286
-
287
- Run:
288
- ```bash
289
- bun test tests/api/register.test.ts
290
- ```
291
- Expected: FAIL because handler/validation do not exist.
292
-
293
- **Step 3: Implement validation + handler**
294
-
295
- Create `lib/validations/auth.ts`:
296
- ```ts
297
- import { z } from "zod";
298
-
299
- export const passwordSchema = z
300
- .string()
301
- .min(8, "密码至少需要 8 个字符")
302
- .regex(/[A-Z]/, "密码必须包含大写字母")
303
- .regex(/[a-z]/, "密码必须包含小写字母")
304
- .regex(/[0-9!@#$%^&*]/, "密码必须包含数字或特殊字符");
305
-
306
- export const registerSchema = z.object({
307
- name: z.string().min(1, "请输入您的姓名").max(100),
308
- email: z.string().email("请输入有效的邮箱地址").max(255).toLowerCase(),
309
- password: passwordSchema,
310
- });
311
-
312
- export type RegisterInput = z.infer<typeof registerSchema>;
313
- ```
314
-
315
- Create `app/api/auth/register/handler.ts`:
316
- ```ts
317
- import { NextResponse } from "next/server";
318
- import { randomUUID } from "crypto";
319
- import { APIError } from "better-auth/api";
320
- import { registerSchema } from "@/lib/validations/auth";
321
- import { generateSlug } from "@/lib/utils/slug";
322
- import { organizations, organizationMembers, userProfiles } from "@/lib/db/schema";
323
-
324
- type RegisterDeps = {
325
- auth: { api: { signUpEmail: (args: any) => Promise<{ headers: Headers; response: Response }> } };
326
- db: { transaction: <T>(fn: (tx: any) => Promise<T>) => Promise<T> };
327
- };
328
-
329
- export function buildRegisterHandler(deps: RegisterDeps) {
330
- return async function POST(req: Request) {
331
- try {
332
- const body = await req.json();
333
- const parsed = registerSchema.safeParse(body);
334
- if (!parsed.success) {
335
- return NextResponse.json(
336
- {
337
- error: "Invalid request body",
338
- code: "VALIDATION_ERROR",
339
- details: parsed.error.issues,
340
- },
341
- { status: 400 }
342
- );
343
- }
344
-
345
- const { name, email, password } = parsed.data;
346
-
347
- const { headers, response } = await deps.auth.api.signUpEmail({
348
- returnHeaders: true,
349
- body: { name, email, password },
350
- });
351
-
352
- const authPayload = await response.json();
353
- const userId = authPayload?.user?.id;
354
-
355
- if (!userId) {
356
- return NextResponse.json(
357
- { error: "Registration failed", code: "REGISTRATION_FAILED" },
358
- { status: 500 }
359
- );
360
- }
361
-
362
- const orgName = `${name}'s Organization`;
363
- const orgSlug = generateSlug(orgName);
364
-
365
- const organizationId = randomUUID();
366
-
367
- await deps.db.transaction(async (tx) => {
368
- await tx.insert(userProfiles).values({ userId, name });
369
- await tx.insert(organizations).values({ id: organizationId, name: orgName, slug: orgSlug });
370
- await tx.insert(organizationMembers).values({ organizationId, userId, role: "admin" });
371
- });
372
-
373
- const res = NextResponse.json(
374
- {
375
- data: { user: authPayload.user },
376
- message: "Registration successful",
377
- },
378
- { status: 201 }
379
- );
380
-
381
- const setCookie = headers.get("set-cookie");
382
- if (setCookie) {
383
- res.headers.set("set-cookie", setCookie);
384
- }
385
-
386
- return res;
387
- } catch (error) {
388
- if (error instanceof APIError) {
389
- if (error.status === 409) {
390
- return NextResponse.json(
391
- { error: "邮箱已存在", code: "EMAIL_EXISTS" },
392
- { status: 409 }
393
- );
394
- }
395
-
396
- return NextResponse.json(
397
- { error: error.message, code: "AUTH_ERROR" },
398
- { status: error.status ?? 400 }
399
- );
400
- }
401
-
402
- return NextResponse.json(
403
- { error: "Registration failed", code: "REGISTRATION_FAILED" },
404
- { status: 500 }
405
- );
406
- }
407
- };
408
- }
409
- ```
410
-
411
- Create `app/api/auth/register/route.ts`:
412
- ```ts
413
- import { auth } from "@/lib/auth/config";
414
- import { db } from "@/lib/db";
415
- import { buildRegisterHandler } from "./handler";
416
-
417
- if (!db) {
418
- throw new Error("DATABASE_URL is not configured");
419
- }
420
-
421
- export const POST = buildRegisterHandler({ auth, db });
422
- ```
423
-
424
- **Step 4: Run tests to verify pass**
425
-
426
- Run:
427
- ```bash
428
- bun test tests/api/register.test.ts
429
- ```
430
- Expected: PASS.
431
-
432
- **Step 5: Commit**
433
-
434
- ```bash
435
- git add lib/validations/auth.ts app/api/auth/register/handler.ts app/api/auth/register/route.ts tests/api/register.test.ts
436
- git commit -m "feat: add register api with validation"
437
- ```
438
-
439
- ---
440
-
441
- ### Task 4: Register UI
442
-
443
- **Files:**
444
- - Create: `app/(auth)/register/page.tsx`
445
- - Create: `components/auth/register-form.tsx`
446
-
447
- **Step 1: Implement UI**
448
-
449
- Create `app/(auth)/register/page.tsx`:
450
- ```tsx
451
- import { headers } from "next/headers";
452
- import { redirect } from "next/navigation";
453
- import { auth } from "@/lib/auth/config";
454
- import { RegisterForm } from "@/components/auth/register-form";
455
-
456
- export default async function RegisterPage() {
457
- const session = await auth.api.getSession({ headers: await headers() });
458
- if (session) redirect("/dashboard");
459
-
460
- return (
461
- <div className="min-h-screen bg-slate-50 flex items-center justify-center px-4">
462
- <div className="w-full max-w-md">
463
- <div className="text-center mb-6">
464
- <h1 className="text-3xl font-semibold">Echo</h1>
465
- <p className="text-sm text-muted-foreground">创建新账户以继续</p>
466
- </div>
467
- <RegisterForm />
468
- </div>
469
- </div>
470
- );
471
- }
472
- ```
473
-
474
- Create `components/auth/register-form.tsx`:
475
- ```tsx
476
- "use client";
477
-
478
- import { useState } from "react";
479
- import { useRouter } from "next/navigation";
480
- import { Button } from "@/components/ui/button";
481
- import { Input } from "@/components/ui/input";
482
- import { Label } from "@/components/ui/label";
483
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
484
-
485
- export function RegisterForm() {
486
- const router = useRouter();
487
- const [isLoading, setIsLoading] = useState(false);
488
- const [errors, setErrors] = useState<Record<string, string>>({});
489
- const [formData, setFormData] = useState({ name: "", email: "", password: "", confirmPassword: "" });
490
-
491
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
492
- const { name, value } = e.target;
493
- setFormData((prev) => ({ ...prev, [name]: value }));
494
- setErrors((prev) => ({ ...prev, [name]: "" }));
495
- };
496
-
497
- const validateForm = () => {
498
- const nextErrors: Record<string, string> = {};
499
- if (!formData.name.trim()) nextErrors.name = "请输入您的姓名";
500
- if (!formData.email) nextErrors.email = "请输入邮箱地址";
501
- if (!formData.password) nextErrors.password = "请输入密码";
502
- if (formData.password !== formData.confirmPassword) nextErrors.confirmPassword = "两次输入的密码不一致";
503
- setErrors(nextErrors);
504
- return Object.keys(nextErrors).length === 0;
505
- };
506
-
507
- const onSubmit = async (e: React.FormEvent) => {
508
- e.preventDefault();
509
- if (!validateForm()) return;
510
-
511
- setIsLoading(true);
512
- try {
513
- const res = await fetch("/api/auth/register", {
514
- method: "POST",
515
- headers: { "Content-Type": "application/json" },
516
- body: JSON.stringify({
517
- name: formData.name,
518
- email: formData.email,
519
- password: formData.password,
520
- }),
521
- });
522
-
523
- const json = await res.json();
524
- if (!res.ok) {
525
- if (json.code === "VALIDATION_ERROR") {
526
- const fieldErrors: Record<string, string> = {};
527
- for (const issue of json.details ?? []) {
528
- const key = issue.path?.[0];
529
- if (key) fieldErrors[key] = issue.message;
530
- }
531
- setErrors(fieldErrors);
532
- } else if (json.code === "EMAIL_EXISTS") {
533
- setErrors({ email: "邮箱已存在" });
534
- }
535
- return;
536
- }
537
-
538
- router.push("/dashboard");
539
- } finally {
540
- setIsLoading(false);
541
- }
542
- };
543
-
544
- return (
545
- <Card>
546
- <CardHeader>
547
- <CardTitle>创建账户</CardTitle>
548
- <CardDescription>填写以下信息注册新账户</CardDescription>
549
- </CardHeader>
550
- <CardContent>
551
- <form onSubmit={onSubmit} className="space-y-4">
552
- <div className="space-y-2">
553
- <Label htmlFor="name">姓名</Label>
554
- <Input id="name" name="name" value={formData.name} onChange={handleChange} disabled={isLoading} />
555
- {errors.name ? <p className="text-sm text-destructive">{errors.name}</p> : null}
556
- </div>
557
- <div className="space-y-2">
558
- <Label htmlFor="email">邮箱</Label>
559
- <Input id="email" name="email" type="email" value={formData.email} onChange={handleChange} disabled={isLoading} />
560
- {errors.email ? <p className="text-sm text-destructive">{errors.email}</p> : null}
561
- </div>
562
- <div className="space-y-2">
563
- <Label htmlFor="password">密码</Label>
564
- <Input id="password" name="password" type="password" value={formData.password} onChange={handleChange} disabled={isLoading} />
565
- {errors.password ? <p className="text-sm text-destructive">{errors.password}</p> : null}
566
- </div>
567
- <div className="space-y-2">
568
- <Label htmlFor="confirmPassword">确认密码</Label>
569
- <Input id="confirmPassword" name="confirmPassword" type="password" value={formData.confirmPassword} onChange={handleChange} disabled={isLoading} />
570
- {errors.confirmPassword ? <p className="text-sm text-destructive">{errors.confirmPassword}</p> : null}
571
- </div>
572
- <Button type="submit" className="w-full" disabled={isLoading}>
573
- {isLoading ? "注册中..." : "注册"}
574
- </Button>
575
- <p className="text-center text-sm text-muted-foreground">
576
- 已有账户?<a className="text-primary" href="/login">登录</a>
577
- </p>
578
- </form>
579
- </CardContent>
580
- </Card>
581
- );
582
- }
583
- ```
584
-
585
- **Step 2: Manual verification**
586
-
587
- Run:
588
- ```bash
589
- bun dev
590
- ```
591
- Check:
592
- - `/register` renders correctly on mobile/desktop.
593
- - Invalid inputs show inline errors.
594
- - Successful submit redirects to `/dashboard`.
595
-
596
- **Step 3: Commit**
597
-
598
- ```bash
599
- git add app/(auth)/register/page.tsx components/auth/register-form.tsx
600
- git commit -m "feat: add register page and form"
601
- ```
602
-
603
- ---
604
-
605
- ### Task 5: Migration apply + smoke check
606
-
607
- **Files:**
608
- - None (commands only)
609
-
610
- **Step 1: Apply migrations locally**
611
-
612
- Run:
613
- ```bash
614
- bun run db:migrate
615
- ```
616
- Expected: migrations applied successfully.
617
-
618
- **Step 2: Smoke test**
619
-
620
- Run:
621
- ```bash
622
- bun test
623
- ```
624
- Expected: all tests pass (existing warning-only lint is acceptable).
625
-
626
- **Step 3: Commit (if any artifacts)**
627
-
628
- Only if new migration metadata or files are created.
@@ -1,20 +0,0 @@
1
- # Roles and Permissions Design
2
-
3
- **Date:** 2026-01-03
4
-
5
- ## Goal
6
- Define a minimal RBAC module with explicit roles and permissions, and align the user schema and validation with the role model.
7
-
8
- ## Architecture
9
- We will add a pure, side-effect-free RBAC module at `lib/auth/permissions.ts`. It will export a `UserRole` union type, a `PERMISSIONS` constant map, a `Permission` union derived from the map, and a `ROLE_PERMISSIONS` record that defines which roles can perform each action. The module will expose `hasPermission(role, permission)` and `canSubmitOnBehalf(role)` helpers. The design is intentionally simple: no I/O, no session coupling, and conservative defaults (unknown roles return false). This provides a stable base for middleware and UI gating without over-engineering.
10
-
11
- To keep data models consistent, we will add a `role` column to the Drizzle `user` table definition in `lib/db/schema/auth.ts`, defaulting to `customer`. We will also add a Zod `userRoleSchema` (and type export) in `lib/validations/auth.ts` so validation and user input handling are aligned with the same role list. The `customer` role represents end users and is granted only `CREATE_FEEDBACK`.
12
-
13
- ## Data Flow
14
- Callers pass a role string and permission identifier to the RBAC module. The module returns a boolean. No database or external services are involved.
15
-
16
- ## Error Handling
17
- Unknown roles or missing mappings return false. This prevents accidental over-permissioning.
18
-
19
- ## Testing
20
- Unit tests will cover role-to-permission mapping, `hasPermission`, and `canSubmitOnBehalf`. Validation tests will assert `userRoleSchema` accepts known roles and rejects unknown ones.