@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,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.
|