@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.
- package/CHANGELOG.md +13 -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/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 -116
- 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,437 +0,0 @@
|
|
|
1
|
-
# User Login 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 login page and form that authenticate via better-auth email/password, support “remember me” (30-day session), and redirect logged-in users to `/dashboard`.
|
|
6
|
-
|
|
7
|
-
**Architecture:** Use better-auth built-in `/api/auth/sign-in/email` endpoint and `auth.api.getSession` for server-side redirect. Client form validates input with a shared Zod schema and submits JSON to the auth endpoint. Session duration is configured in `lib/auth/config.ts`.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Next.js App Router, React 19, TypeScript, Tailwind, better-auth, Zod, Bun.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
### Task 1: Add login validation schema (TDD)
|
|
14
|
-
|
|
15
|
-
**Files:**
|
|
16
|
-
- Modify: `lib/validations/auth.ts`
|
|
17
|
-
- Create: `tests/lib/login-schema.test.ts`
|
|
18
|
-
|
|
19
|
-
**Step 1: Write the failing test**
|
|
20
|
-
|
|
21
|
-
`tests/lib/login-schema.test.ts`
|
|
22
|
-
```ts
|
|
23
|
-
import { describe, expect, it } from "bun:test";
|
|
24
|
-
import { loginSchema } from "@/lib/validations/auth";
|
|
25
|
-
|
|
26
|
-
describe("loginSchema", () => {
|
|
27
|
-
it("rejects empty email and password", () => {
|
|
28
|
-
const result = loginSchema.safeParse({ email: "", password: "" });
|
|
29
|
-
expect(result.success).toBe(false);
|
|
30
|
-
if (!result.success) {
|
|
31
|
-
const messages = result.error.issues.map((issue) => issue.message);
|
|
32
|
-
expect(messages).toContain("请输入邮箱地址");
|
|
33
|
-
expect(messages).toContain("请输入密码");
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("accepts valid email and password", () => {
|
|
38
|
-
const result = loginSchema.safeParse({ email: "user@example.com", password: "Password123" });
|
|
39
|
-
expect(result.success).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
**Step 2: Run test to verify it fails**
|
|
45
|
-
|
|
46
|
-
Run: `bun test tests/lib/login-schema.test.ts`
|
|
47
|
-
Expected: FAIL because `loginSchema` is not defined.
|
|
48
|
-
|
|
49
|
-
**Step 3: Write minimal implementation**
|
|
50
|
-
|
|
51
|
-
`lib/validations/auth.ts`
|
|
52
|
-
```ts
|
|
53
|
-
export const loginSchema = z.object({
|
|
54
|
-
email: z
|
|
55
|
-
.string()
|
|
56
|
-
.min(1, "请输入邮箱地址")
|
|
57
|
-
.email("请输入有效的邮箱地址")
|
|
58
|
-
.max(255)
|
|
59
|
-
.toLowerCase(),
|
|
60
|
-
password: z.string().min(1, "请输入密码"),
|
|
61
|
-
rememberMe: z.boolean().optional(),
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
export type LoginInput = z.infer<typeof loginSchema>;
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
**Step 4: Run test to verify it passes**
|
|
68
|
-
|
|
69
|
-
Run: `bun test tests/lib/login-schema.test.ts`
|
|
70
|
-
Expected: PASS.
|
|
71
|
-
|
|
72
|
-
**Step 5: Commit**
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
git add lib/validations/auth.ts tests/lib/login-schema.test.ts
|
|
76
|
-
git commit -m "test: add login schema validation"
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
### Task 2: Configure session expiry for remember-me
|
|
82
|
-
|
|
83
|
-
**Files:**
|
|
84
|
-
- Modify: `lib/auth/config.ts`
|
|
85
|
-
|
|
86
|
-
**Step 1: Write the failing test**
|
|
87
|
-
|
|
88
|
-
Create a simple configuration expectation test.
|
|
89
|
-
|
|
90
|
-
`tests/lib/auth-config.test.ts`
|
|
91
|
-
```ts
|
|
92
|
-
import { describe, expect, it } from "bun:test";
|
|
93
|
-
import { auth } from "@/lib/auth/config";
|
|
94
|
-
|
|
95
|
-
describe("auth session config", () => {
|
|
96
|
-
it("uses 30-day session expiry", () => {
|
|
97
|
-
expect(auth.options.session?.expiresIn).toBe(60 * 60 * 24 * 30);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
**Step 2: Run test to verify it fails**
|
|
103
|
-
|
|
104
|
-
Run: `bun test tests/lib/auth-config.test.ts`
|
|
105
|
-
Expected: FAIL because `expiresIn` is undefined.
|
|
106
|
-
|
|
107
|
-
**Step 3: Write minimal implementation**
|
|
108
|
-
|
|
109
|
-
`lib/auth/config.ts`
|
|
110
|
-
```ts
|
|
111
|
-
session: {
|
|
112
|
-
expiresIn: 60 * 60 * 24 * 30,
|
|
113
|
-
},
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
**Step 4: Run test to verify it passes**
|
|
117
|
-
|
|
118
|
-
Run: `bun test tests/lib/auth-config.test.ts`
|
|
119
|
-
Expected: PASS.
|
|
120
|
-
|
|
121
|
-
**Step 5: Commit**
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
git add lib/auth/config.ts tests/lib/auth-config.test.ts
|
|
125
|
-
git commit -m "feat: set remember-me session duration"
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
### Task 3: Create login page (server component)
|
|
131
|
-
|
|
132
|
-
**Files:**
|
|
133
|
-
- Create: `app/(auth)/login/page.tsx`
|
|
134
|
-
|
|
135
|
-
**Step 1: Write the failing test**
|
|
136
|
-
|
|
137
|
-
Create a basic render test to ensure the page uses the LoginForm component.
|
|
138
|
-
|
|
139
|
-
`tests/app/login-page.test.ts`
|
|
140
|
-
```ts
|
|
141
|
-
import { describe, expect, it } from "bun:test";
|
|
142
|
-
import LoginPage from "@/app/(auth)/login/page";
|
|
143
|
-
|
|
144
|
-
// This is a shallow check for export existence.
|
|
145
|
-
// Actual redirect behavior is validated manually.
|
|
146
|
-
|
|
147
|
-
describe("LoginPage", () => {
|
|
148
|
-
it("is a function", () => {
|
|
149
|
-
expect(typeof LoginPage).toBe("function");
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
**Step 2: Run test to verify it fails**
|
|
155
|
-
|
|
156
|
-
Run: `bun test tests/app/login-page.test.ts`
|
|
157
|
-
Expected: FAIL because the file does not exist.
|
|
158
|
-
|
|
159
|
-
**Step 3: Write minimal implementation**
|
|
160
|
-
|
|
161
|
-
`app/(auth)/login/page.tsx`
|
|
162
|
-
```tsx
|
|
163
|
-
import { headers } from "next/headers";
|
|
164
|
-
import { redirect } from "next/navigation";
|
|
165
|
-
import { LoginForm } from "@/components/auth/login-form";
|
|
166
|
-
import { auth } from "@/lib/auth/config";
|
|
167
|
-
|
|
168
|
-
export default async function LoginPage() {
|
|
169
|
-
const session = await auth.api.getSession({ headers: await headers() });
|
|
170
|
-
|
|
171
|
-
if (session) {
|
|
172
|
-
redirect("/dashboard");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return (
|
|
176
|
-
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 px-4 py-12">
|
|
177
|
-
<div className="mx-auto flex w-full max-w-md flex-col gap-6">
|
|
178
|
-
<div className="text-center">
|
|
179
|
-
<h1 className="text-3xl font-semibold tracking-tight text-slate-900">
|
|
180
|
-
Echo
|
|
181
|
-
</h1>
|
|
182
|
-
<p className="mt-2 text-sm text-slate-600">
|
|
183
|
-
登录以继续访问你的反馈和组织
|
|
184
|
-
</p>
|
|
185
|
-
</div>
|
|
186
|
-
<LoginForm />
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
**Step 4: Run test to verify it passes**
|
|
194
|
-
|
|
195
|
-
Run: `bun test tests/app/login-page.test.ts`
|
|
196
|
-
Expected: PASS.
|
|
197
|
-
|
|
198
|
-
**Step 5: Commit**
|
|
199
|
-
|
|
200
|
-
```bash
|
|
201
|
-
git add app/(auth)/login/page.tsx tests/app/login-page.test.ts
|
|
202
|
-
git commit -m "feat: add login page"
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
---
|
|
206
|
-
|
|
207
|
-
### Task 4: Create login form (client component)
|
|
208
|
-
|
|
209
|
-
**Files:**
|
|
210
|
-
- Create: `components/auth/login-form.tsx`
|
|
211
|
-
|
|
212
|
-
**Step 1: Write the failing test**
|
|
213
|
-
|
|
214
|
-
Test validation behavior and error messaging without a DOM by using the shared schema.
|
|
215
|
-
|
|
216
|
-
`tests/components/login-form-validation.test.ts`
|
|
217
|
-
```ts
|
|
218
|
-
import { describe, expect, it } from "bun:test";
|
|
219
|
-
import { loginSchema } from "@/lib/validations/auth";
|
|
220
|
-
|
|
221
|
-
describe("LoginForm validation", () => {
|
|
222
|
-
it("rejects invalid email format", () => {
|
|
223
|
-
const result = loginSchema.safeParse({ email: "invalid", password: "pass" });
|
|
224
|
-
expect(result.success).toBe(false);
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
**Step 2: Run test to verify it fails**
|
|
230
|
-
|
|
231
|
-
Run: `bun test tests/components/login-form-validation.test.ts`
|
|
232
|
-
Expected: FAIL because login form/schema not yet wired (schema exists but test file missing).
|
|
233
|
-
|
|
234
|
-
**Step 3: Write minimal implementation**
|
|
235
|
-
|
|
236
|
-
`components/auth/login-form.tsx`
|
|
237
|
-
```tsx
|
|
238
|
-
"use client";
|
|
239
|
-
|
|
240
|
-
import { useState } from "react";
|
|
241
|
-
import { useRouter } from "next/navigation";
|
|
242
|
-
import { Button } from "@/components/ui/button";
|
|
243
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
244
|
-
import { Checkbox } from "@/components/ui/checkbox";
|
|
245
|
-
import { Input } from "@/components/ui/input";
|
|
246
|
-
import { Label } from "@/components/ui/label";
|
|
247
|
-
import { loginSchema, type LoginInput } from "@/lib/validations/auth";
|
|
248
|
-
|
|
249
|
-
export function LoginForm() {
|
|
250
|
-
const router = useRouter();
|
|
251
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
252
|
-
const [formError, setFormError] = useState<string | null>(null);
|
|
253
|
-
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
254
|
-
const [formData, setFormData] = useState<LoginInput>({
|
|
255
|
-
email: "",
|
|
256
|
-
password: "",
|
|
257
|
-
rememberMe: false,
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
261
|
-
const { name, value, type, checked } = e.target;
|
|
262
|
-
setFormData((prev) => ({
|
|
263
|
-
...prev,
|
|
264
|
-
[name]: type === "checkbox" ? checked : value,
|
|
265
|
-
}));
|
|
266
|
-
setErrors((prev) => ({ ...prev, [name]: "" }));
|
|
267
|
-
setFormError(null);
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
const validateForm = () => {
|
|
271
|
-
const result = loginSchema.safeParse(formData);
|
|
272
|
-
if (result.success) {
|
|
273
|
-
setErrors({});
|
|
274
|
-
return true;
|
|
275
|
-
}
|
|
276
|
-
const nextErrors: Record<string, string> = {};
|
|
277
|
-
for (const issue of result.error.issues) {
|
|
278
|
-
const key = issue.path?.[0];
|
|
279
|
-
if (typeof key === "string") {
|
|
280
|
-
nextErrors[key] = issue.message;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
setErrors(nextErrors);
|
|
284
|
-
return false;
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
const handleSubmit = async (event: React.FormEvent) => {
|
|
288
|
-
event.preventDefault();
|
|
289
|
-
setFormError(null);
|
|
290
|
-
|
|
291
|
-
if (!validateForm()) {
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
setIsLoading(true);
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
const res = await fetch("/api/auth/sign-in/email", {
|
|
299
|
-
method: "POST",
|
|
300
|
-
headers: { "Content-Type": "application/json" },
|
|
301
|
-
body: JSON.stringify({
|
|
302
|
-
email: formData.email,
|
|
303
|
-
password: formData.password,
|
|
304
|
-
rememberMe: formData.rememberMe,
|
|
305
|
-
}),
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const json = await res.json();
|
|
309
|
-
|
|
310
|
-
if (!res.ok) {
|
|
311
|
-
throw new Error(json?.message ?? "邮箱或密码错误");
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
router.push("/dashboard");
|
|
315
|
-
} catch {
|
|
316
|
-
setFormError("邮箱或密码错误");
|
|
317
|
-
} finally {
|
|
318
|
-
setIsLoading(false);
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
return (
|
|
323
|
-
<Card>
|
|
324
|
-
<CardHeader>
|
|
325
|
-
<CardTitle>登录</CardTitle>
|
|
326
|
-
<CardDescription>输入邮箱和密码登录</CardDescription>
|
|
327
|
-
</CardHeader>
|
|
328
|
-
<CardContent>
|
|
329
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
330
|
-
{formError ? (
|
|
331
|
-
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
332
|
-
{formError}
|
|
333
|
-
</div>
|
|
334
|
-
) : null}
|
|
335
|
-
|
|
336
|
-
<div className="space-y-2">
|
|
337
|
-
<Label htmlFor="email">邮箱</Label>
|
|
338
|
-
<Input
|
|
339
|
-
id="email"
|
|
340
|
-
name="email"
|
|
341
|
-
type="email"
|
|
342
|
-
placeholder="you@example.com"
|
|
343
|
-
value={formData.email}
|
|
344
|
-
onChange={handleChange}
|
|
345
|
-
disabled={isLoading}
|
|
346
|
-
className={errors.email ? "border-destructive" : ""}
|
|
347
|
-
/>
|
|
348
|
-
{errors.email ? (
|
|
349
|
-
<p className="text-sm text-destructive">{errors.email}</p>
|
|
350
|
-
) : null}
|
|
351
|
-
</div>
|
|
352
|
-
|
|
353
|
-
<div className="space-y-2">
|
|
354
|
-
<Label htmlFor="password">密码</Label>
|
|
355
|
-
<Input
|
|
356
|
-
id="password"
|
|
357
|
-
name="password"
|
|
358
|
-
type="password"
|
|
359
|
-
value={formData.password}
|
|
360
|
-
onChange={handleChange}
|
|
361
|
-
disabled={isLoading}
|
|
362
|
-
className={errors.password ? "border-destructive" : ""}
|
|
363
|
-
/>
|
|
364
|
-
{errors.password ? (
|
|
365
|
-
<p className="text-sm text-destructive">{errors.password}</p>
|
|
366
|
-
) : null}
|
|
367
|
-
</div>
|
|
368
|
-
|
|
369
|
-
<div className="flex items-center justify-between">
|
|
370
|
-
<div className="flex items-center space-x-2">
|
|
371
|
-
<Checkbox
|
|
372
|
-
id="rememberMe"
|
|
373
|
-
name="rememberMe"
|
|
374
|
-
checked={formData.rememberMe}
|
|
375
|
-
onCheckedChange={(checked) =>
|
|
376
|
-
setFormData((prev) => ({ ...prev, rememberMe: checked === true }))
|
|
377
|
-
}
|
|
378
|
-
disabled={isLoading}
|
|
379
|
-
/>
|
|
380
|
-
<Label htmlFor="rememberMe" className="text-sm font-normal">
|
|
381
|
-
记住我
|
|
382
|
-
</Label>
|
|
383
|
-
</div>
|
|
384
|
-
<a className="text-sm text-primary hover:underline" href="/forgot-password">
|
|
385
|
-
忘记密码?
|
|
386
|
-
</a>
|
|
387
|
-
</div>
|
|
388
|
-
|
|
389
|
-
<Button type="submit" className="w-full" disabled={isLoading}>
|
|
390
|
-
{isLoading ? "登录中..." : "登录"}
|
|
391
|
-
</Button>
|
|
392
|
-
|
|
393
|
-
<p className="text-center text-sm text-muted-foreground">
|
|
394
|
-
还没有账户?
|
|
395
|
-
<a className="ml-1 text-primary hover:underline" href="/register">
|
|
396
|
-
注册
|
|
397
|
-
</a>
|
|
398
|
-
</p>
|
|
399
|
-
</form>
|
|
400
|
-
</CardContent>
|
|
401
|
-
</Card>
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
**Step 4: Run test to verify it passes**
|
|
407
|
-
|
|
408
|
-
Run: `bun test tests/components/login-form-validation.test.ts`
|
|
409
|
-
Expected: PASS.
|
|
410
|
-
|
|
411
|
-
**Step 5: Commit**
|
|
412
|
-
|
|
413
|
-
```bash
|
|
414
|
-
git add components/auth/login-form.tsx tests/components/login-form-validation.test.ts
|
|
415
|
-
git commit -m "feat: add login form"
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
---
|
|
419
|
-
|
|
420
|
-
### Task 5: Full verification
|
|
421
|
-
|
|
422
|
-
**Step 1: Run lint**
|
|
423
|
-
|
|
424
|
-
Run: `bun run lint`
|
|
425
|
-
Expected: PASS.
|
|
426
|
-
|
|
427
|
-
**Step 2: Run full test suite**
|
|
428
|
-
|
|
429
|
-
Run: `bun test`
|
|
430
|
-
Expected: PASS.
|
|
431
|
-
|
|
432
|
-
**Step 3: Commit (if any changes)**
|
|
433
|
-
|
|
434
|
-
```bash
|
|
435
|
-
git add -A
|
|
436
|
-
git commit -m "chore: verify login flow"
|
|
437
|
-
```
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# 用户注册设计说明
|
|
2
|
-
|
|
3
|
-
**日期:** 2026-01-02
|
|
4
|
-
**范围:** Story 4.1 用户注册(认证域 + 业务域)
|
|
5
|
-
|
|
6
|
-
## 目标
|
|
7
|
-
为新用户提供注册入口,完成账号创建、默认组织创建与自动登录,并保持认证域与业务域解耦。
|
|
8
|
-
|
|
9
|
-
## 架构与边界
|
|
10
|
-
- **认证域(better-auth)**:用户/会话等授权表由 better-auth 的 Drizzle schema 提供与维护。
|
|
11
|
-
- **业务域(自有表)**:组织与组织成员表独立维护;新增 `user_profiles` 保存姓名等扩展信息。
|
|
12
|
-
- **迁移策略**:统一通过 drizzle-kit 生成并运行迁移(不手写 SQL)。
|
|
13
|
-
|
|
14
|
-
## 数据模型
|
|
15
|
-
- `auth_*`(better-auth 提供):用户、会话等表结构由其 Drizzle schema 生成。
|
|
16
|
-
- `user_profiles`
|
|
17
|
-
- `userId` 外键指向认证域用户表
|
|
18
|
-
- `name`(必填)及未来可扩展字段
|
|
19
|
-
- `organizations`
|
|
20
|
-
- `name`, `slug`
|
|
21
|
-
- `organization_members`
|
|
22
|
-
- `organizationId`, `userId`, `role`(注册时为 `admin`)
|
|
23
|
-
|
|
24
|
-
## 注册流程
|
|
25
|
-
1. 客户端提交姓名/邮箱/密码到 `POST /api/auth/register`。
|
|
26
|
-
2. 服务端使用 Zod 校验(邮箱格式/密码强度/姓名)。
|
|
27
|
-
3. 调用 better-auth 注册接口创建用户并建立会话。
|
|
28
|
-
4. 同一事务内创建 `user_profiles`、默认组织与成员关系。
|
|
29
|
-
5. 成功返回 201,前端跳转 `/dashboard`。
|
|
30
|
-
|
|
31
|
-
## 错误处理
|
|
32
|
-
- 校验失败:400 + 字段级错误。
|
|
33
|
-
- 邮箱已存在:409。
|
|
34
|
-
- 其他错误:500(通用错误提示)。
|
|
35
|
-
|
|
36
|
-
## 前端
|
|
37
|
-
- 页面:`app/(auth)/register/page.tsx`
|
|
38
|
-
- 表单组件:`components/auth/register-form.tsx`
|
|
39
|
-
- 客户端校验 + 服务端再次校验
|
|
40
|
-
- 成功后提示 toast 并跳转仪表板
|
|
41
|
-
|
|
42
|
-
## 测试范围
|
|
43
|
-
- API:成功注册、邮箱重复、弱密码/无效邮箱。
|
|
44
|
-
- 验证创建了 profile、组织与成员关系。
|
|
45
|
-
|
|
46
|
-
## 待确认
|
|
47
|
-
- better-auth Drizzle schema 的具体导出与命名(需以官方文档为准)。
|