nextworks 0.1.0-alpha.11 → 0.1.0-alpha.14
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/README.md +20 -9
- package/dist/kits/auth-core/.nextworks/docs/AUTH_CORE_README.md +3 -3
- package/dist/kits/auth-core/.nextworks/docs/AUTH_QUICKSTART.md +264 -244
- package/dist/kits/auth-core/app/(protected)/settings/profile/profile-form.tsx +120 -114
- package/dist/kits/auth-core/app/api/auth/forgot-password/route.ts +116 -114
- package/dist/kits/auth-core/app/api/auth/reset-password/route.ts +66 -63
- package/dist/kits/auth-core/app/api/auth/send-verify-email/route.ts +1 -1
- package/dist/kits/auth-core/app/api/users/[id]/route.ts +134 -127
- package/dist/kits/auth-core/app/auth/reset-password/page.tsx +186 -187
- package/dist/kits/auth-core/components/auth/dashboard.tsx +25 -2
- package/dist/kits/auth-core/components/auth/forgot-password-form.tsx +90 -90
- package/dist/kits/auth-core/components/auth/login-form.tsx +492 -467
- package/dist/kits/auth-core/components/auth/signup-form.tsx +28 -29
- package/dist/kits/auth-core/lib/auth.ts +46 -15
- package/dist/kits/auth-core/lib/forms/map-errors.ts +37 -11
- package/dist/kits/auth-core/lib/server/result.ts +45 -45
- package/dist/kits/auth-core/lib/validation/forms.ts +1 -2
- package/dist/kits/auth-core/package-deps.json +4 -2
- package/dist/kits/auth-core/types/next-auth.d.ts +1 -1
- package/dist/kits/blocks/.nextworks/docs/BLOCKS_QUICKSTART.md +2 -8
- package/dist/kits/blocks/.nextworks/docs/THEME_GUIDE.md +18 -1
- package/dist/kits/blocks/app/templates/productlaunch/page.tsx +0 -2
- package/dist/kits/blocks/components/sections/FAQ.tsx +0 -1
- package/dist/kits/blocks/components/sections/Newsletter.tsx +2 -2
- package/dist/kits/blocks/components/ui/switch.tsx +78 -78
- package/dist/kits/blocks/components/ui/theme-selector.tsx +1 -1
- package/dist/kits/blocks/lib/themes.ts +1 -0
- package/dist/kits/blocks/package-deps.json +4 -4
- package/dist/kits/data/.nextworks/docs/DATA_QUICKSTART.md +128 -112
- package/dist/kits/data/.nextworks/docs/DATA_README.md +2 -1
- package/dist/kits/data/app/api/posts/[id]/route.ts +83 -83
- package/dist/kits/data/app/api/posts/route.ts +136 -138
- package/dist/kits/data/app/api/seed-demo/route.ts +1 -2
- package/dist/kits/data/app/api/users/[id]/route.ts +29 -17
- package/dist/kits/data/app/api/users/check-email/route.ts +1 -1
- package/dist/kits/data/app/api/users/check-unique/route.ts +30 -27
- package/dist/kits/data/app/api/users/route.ts +0 -2
- package/dist/kits/data/app/examples/demo/create-post-form.tsx +108 -106
- package/dist/kits/data/app/examples/demo/page.tsx +2 -1
- package/dist/kits/data/app/examples/demo/seed-demo-button.tsx +1 -1
- package/dist/kits/data/components/admin/posts-manager.tsx +727 -719
- package/dist/kits/data/components/admin/users-manager.tsx +435 -432
- package/dist/kits/data/lib/server/result.ts +5 -2
- package/dist/kits/data/package-deps.json +1 -1
- package/dist/kits/data/scripts/seed-demo.mjs +1 -2
- package/dist/kits/forms/app/api/wizard/route.ts +76 -71
- package/dist/kits/forms/app/examples/forms/server-action/page.tsx +78 -71
- package/dist/kits/forms/components/hooks/useCheckUnique.ts +85 -79
- package/dist/kits/forms/components/ui/form/form-control.tsx +28 -28
- package/dist/kits/forms/components/ui/form/form-description.tsx +23 -22
- package/dist/kits/forms/components/ui/form/form-item.tsx +21 -21
- package/dist/kits/forms/components/ui/form/form-label.tsx +24 -24
- package/dist/kits/forms/components/ui/form/form-message.tsx +28 -29
- package/dist/kits/forms/components/ui/switch.tsx +78 -78
- package/dist/kits/forms/lib/forms/map-errors.ts +1 -1
- package/dist/kits/forms/lib/validation/forms.ts +1 -2
- package/package.json +1 -1
|
@@ -1,127 +1,134 @@
|
|
|
1
|
-
import { NextRequest } from "next/server";
|
|
2
|
-
import { prisma } from "@/lib/prisma";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
await
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
import { prisma } from "@/lib/prisma";
|
|
3
|
+
import { jsonOk, jsonFail } from "@/lib/server/result";
|
|
4
|
+
import { getServerSession } from "next-auth";
|
|
5
|
+
import { authOptions } from "@/lib/auth";
|
|
6
|
+
|
|
7
|
+
// Ensure Prisma runs in Node.js (not Edge)
|
|
8
|
+
export const runtime = "nodejs";
|
|
9
|
+
|
|
10
|
+
type RouteContext = { params: Promise<{ id: string }> };
|
|
11
|
+
|
|
12
|
+
// Type guards/helpers (kept minimal here)
|
|
13
|
+
const hasErrorCode = (e: unknown, code: string): boolean =>
|
|
14
|
+
typeof e === "object" &&
|
|
15
|
+
e !== null &&
|
|
16
|
+
"code" in e &&
|
|
17
|
+
typeof (e as { code?: unknown }).code === "string" &&
|
|
18
|
+
(e as { code: string }).code === code;
|
|
19
|
+
|
|
20
|
+
export async function GET(_req: NextRequest, { params }: RouteContext) {
|
|
21
|
+
try {
|
|
22
|
+
const { id } = await params; // async params in Next.js 15
|
|
23
|
+
|
|
24
|
+
const user = await prisma.user.findUnique({
|
|
25
|
+
where: { id },
|
|
26
|
+
select: { id: true, name: true, email: true, role: true },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!user) {
|
|
30
|
+
return jsonFail("Not found", { status: 404 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return jsonOk(user);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.error("GET /api/users/[id] error:", e);
|
|
36
|
+
return jsonFail("Failed to fetch user", { status: 500 });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function PUT(req: NextRequest, { params }: RouteContext) {
|
|
41
|
+
try {
|
|
42
|
+
// Only admins may update other users. Allow self-update as well.
|
|
43
|
+
const session = await getServerSession(authOptions);
|
|
44
|
+
if (!session?.user) return jsonFail("Unauthorized", { status: 401 });
|
|
45
|
+
|
|
46
|
+
const { id } = await params;
|
|
47
|
+
const isAdmin = (session.user as { role?: string }).role === "admin";
|
|
48
|
+
const isSelf = (session.user as { id?: string }).id === id;
|
|
49
|
+
if (!isAdmin && !isSelf) return jsonFail("Forbidden", { status: 403 });
|
|
50
|
+
|
|
51
|
+
const body: unknown = await req.json();
|
|
52
|
+
if (typeof body !== "object" || body === null) {
|
|
53
|
+
return jsonFail("Body must be a JSON object", { status: 400 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Validate with Zod (imported lazily to keep this route light)
|
|
57
|
+
const { userUpdateSchema } = await import("@/lib/validation/forms");
|
|
58
|
+
try {
|
|
59
|
+
const parsed = userUpdateSchema.parse(body);
|
|
60
|
+
|
|
61
|
+
// Build Prisma-compatible update object (avoid `as any` + incremental mutation)
|
|
62
|
+
const data = {
|
|
63
|
+
...(parsed.name !== undefined
|
|
64
|
+
? { name: parsed.name === "" ? null : parsed.name }
|
|
65
|
+
: {}),
|
|
66
|
+
...(parsed.email !== undefined ? { email: parsed.email } : {}),
|
|
67
|
+
...(parsed.image !== undefined
|
|
68
|
+
? { image: parsed.image === "" ? null : parsed.image }
|
|
69
|
+
: {}),
|
|
70
|
+
...(parsed.password !== undefined
|
|
71
|
+
? {
|
|
72
|
+
password: await (async () => {
|
|
73
|
+
const { hashPassword } = await import("@/lib/hash");
|
|
74
|
+
// `password` is present in this branch; help TS narrow from `string | undefined`.
|
|
75
|
+
return hashPassword(parsed.password!);
|
|
76
|
+
})(),
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
79
|
+
...(parsed.emailVerified !== undefined
|
|
80
|
+
? { emailVerified: parsed.emailVerified }
|
|
81
|
+
: {}),
|
|
82
|
+
} satisfies Parameters<typeof prisma.user.update>[0]["data"];
|
|
83
|
+
|
|
84
|
+
const updated = await prisma.user.update({
|
|
85
|
+
where: { id },
|
|
86
|
+
data,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return jsonOk(updated, { status: 200, message: "User updated" });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
// Zod errors
|
|
92
|
+
if (err && typeof err === "object" && "issues" in (err as any)) {
|
|
93
|
+
const { jsonFromZod } = await import("@/lib/server/result");
|
|
94
|
+
return jsonFromZod(err as any, {
|
|
95
|
+
status: 400,
|
|
96
|
+
message: "Validation failed",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error("PUT /api/users/[id] error:", e);
|
|
103
|
+
if (hasErrorCode(e, "P2025")) {
|
|
104
|
+
return jsonFail("Not found", { status: 404 });
|
|
105
|
+
}
|
|
106
|
+
return jsonFail("Failed to update user", { status: 500 });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function DELETE(_req: NextRequest, { params }: RouteContext) {
|
|
111
|
+
try {
|
|
112
|
+
const { requireAdminApi } = await import("@/lib/auth-helpers");
|
|
113
|
+
const session = await requireAdminApi();
|
|
114
|
+
if (!session) return jsonFail("Forbidden", { status: 403 });
|
|
115
|
+
|
|
116
|
+
const { id } = await params;
|
|
117
|
+
|
|
118
|
+
// Clean up dependent records first to avoid FK violations
|
|
119
|
+
await prisma.$transaction([
|
|
120
|
+
prisma.account.deleteMany({ where: { userId: id } }),
|
|
121
|
+
prisma.session.deleteMany({ where: { userId: id } }),
|
|
122
|
+
prisma.post.deleteMany({ where: { authorId: id } }),
|
|
123
|
+
prisma.user.delete({ where: { id } }),
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
return jsonOk({ ok: true }, { status: 200, message: "User deleted" });
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.error("DELETE /api/users/[id] error:", e);
|
|
129
|
+
if (hasErrorCode(e, "P2025")) {
|
|
130
|
+
return jsonFail("Not found", { status: 404 });
|
|
131
|
+
}
|
|
132
|
+
return jsonFail("Failed to delete user", { status: 500 });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -1,187 +1,186 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useEffect, useState } from "react";
|
|
4
|
-
import { useSearchParams
|
|
5
|
-
import { useForm } from "react-hook-form";
|
|
6
|
-
import { zodResolver } from "@hookform/resolvers/zod";
|
|
7
|
-
import { resetPasswordSchema } from "@/lib/validation/forms";
|
|
8
|
-
import { Input } from "@/components/ui/input";
|
|
9
|
-
import { Button } from "@/components/ui/button";
|
|
10
|
-
import { Form } from "@/components/ui/form/form";
|
|
11
|
-
import { FormField } from "@/components/ui/form/form-field";
|
|
12
|
-
import { FormItem } from "@/components/ui/form/form-item";
|
|
13
|
-
import { FormLabel } from "@/components/ui/form/form-label";
|
|
14
|
-
import { FormControl } from "@/components/ui/form/form-control";
|
|
15
|
-
import { FormMessage } from "@/components/ui/form/form-message";
|
|
16
|
-
import { toast } from "sonner";
|
|
17
|
-
import { signOut } from "next-auth/react";
|
|
18
|
-
|
|
19
|
-
export default function ResetPasswordPage() {
|
|
20
|
-
// Do not gate rendering on process.env here — rendering must be consistent
|
|
21
|
-
// between server and client to avoid hydration mismatches. The API routes
|
|
22
|
-
// already enforce the feature guard (returning 404) so we let the client
|
|
23
|
-
// always render and surface the API's response.
|
|
24
|
-
const searchParams = useSearchParams();
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
console.error("signOut failed:", e);
|
|
75
|
-
}
|
|
76
|
-
// Poll /api/auth/session until it returns null, or timeout after 2s.
|
|
77
|
-
// This avoids the race where signOut is accepted server-side but the
|
|
78
|
-
// client still believes it's authenticated due to caching or timing.
|
|
79
|
-
const waitForSignOut = async (timeout = 2000, interval = 200) => {
|
|
80
|
-
const start = Date.now();
|
|
81
|
-
while (Date.now() - start < timeout) {
|
|
82
|
-
try {
|
|
83
|
-
const r = await fetch("/api/auth/session");
|
|
84
|
-
if (r.ok) {
|
|
85
|
-
const json = await r.json();
|
|
86
|
-
if (!json) {
|
|
87
|
-
// session cleared
|
|
88
|
-
window.location.href = "/auth/login?signup=1";
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
} catch
|
|
93
|
-
// ignore transient fetch errors
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { useSearchParams } from "next/navigation";
|
|
5
|
+
import { useForm } from "react-hook-form";
|
|
6
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
7
|
+
import { resetPasswordSchema } from "@/lib/validation/forms";
|
|
8
|
+
import { Input } from "@/components/ui/input";
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import { Form } from "@/components/ui/form/form";
|
|
11
|
+
import { FormField } from "@/components/ui/form/form-field";
|
|
12
|
+
import { FormItem } from "@/components/ui/form/form-item";
|
|
13
|
+
import { FormLabel } from "@/components/ui/form/form-label";
|
|
14
|
+
import { FormControl } from "@/components/ui/form/form-control";
|
|
15
|
+
import { FormMessage } from "@/components/ui/form/form-message";
|
|
16
|
+
import { toast } from "sonner";
|
|
17
|
+
import { signOut } from "next-auth/react";
|
|
18
|
+
|
|
19
|
+
export default function ResetPasswordPage() {
|
|
20
|
+
// Do not gate rendering on process.env here — rendering must be consistent
|
|
21
|
+
// between server and client to avoid hydration mismatches. The API routes
|
|
22
|
+
// already enforce the feature guard (returning 404) so we let the client
|
|
23
|
+
// always render and surface the API's response.
|
|
24
|
+
const searchParams = useSearchParams();
|
|
25
|
+
const token = searchParams.get("token") || "";
|
|
26
|
+
const [valid, setValid] = useState<boolean | null>(null);
|
|
27
|
+
|
|
28
|
+
type ResetPasswordValues = typeof resetPasswordSchema._type;
|
|
29
|
+
|
|
30
|
+
const methods = useForm<ResetPasswordValues>({
|
|
31
|
+
resolver: zodResolver(resetPasswordSchema),
|
|
32
|
+
defaultValues: { token, password: "", confirmPassword: "" },
|
|
33
|
+
});
|
|
34
|
+
const {
|
|
35
|
+
handleSubmit,
|
|
36
|
+
control,
|
|
37
|
+
formState: { isSubmitting },
|
|
38
|
+
} = methods;
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
(async () => {
|
|
42
|
+
if (!token) return setValid(false);
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(
|
|
45
|
+
`/api/auth/reset-password?token=${encodeURIComponent(token)}`,
|
|
46
|
+
);
|
|
47
|
+
if (res.ok) {
|
|
48
|
+
const json = await res.json();
|
|
49
|
+
setValid(!!json.valid);
|
|
50
|
+
} else {
|
|
51
|
+
setValid(false);
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
setValid(false);
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
}, [token]);
|
|
58
|
+
|
|
59
|
+
const onSubmit = async (data: ResetPasswordValues) => {
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch("/api/auth/reset-password", {
|
|
62
|
+
method: "POST",
|
|
63
|
+
body: JSON.stringify(data),
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
});
|
|
66
|
+
if (res.ok) {
|
|
67
|
+
toast.success("Password updated. You can now sign in.");
|
|
68
|
+
try {
|
|
69
|
+
// Ensure any existing session is cleared so the login page doesn't
|
|
70
|
+
// immediately redirect away. This also avoids confusion during testing.
|
|
71
|
+
await signOut({ redirect: false });
|
|
72
|
+
} catch (e: unknown) {
|
|
73
|
+
// ignore signOut failures but log for debugging
|
|
74
|
+
console.error("signOut failed:", e);
|
|
75
|
+
}
|
|
76
|
+
// Poll /api/auth/session until it returns null, or timeout after 2s.
|
|
77
|
+
// This avoids the race where signOut is accepted server-side but the
|
|
78
|
+
// client still believes it's authenticated due to caching or timing.
|
|
79
|
+
const waitForSignOut = async (timeout = 2000, interval = 200) => {
|
|
80
|
+
const start = Date.now();
|
|
81
|
+
while (Date.now() - start < timeout) {
|
|
82
|
+
try {
|
|
83
|
+
const r = await fetch("/api/auth/session");
|
|
84
|
+
if (r.ok) {
|
|
85
|
+
const json = await r.json();
|
|
86
|
+
if (!json) {
|
|
87
|
+
// session cleared
|
|
88
|
+
window.location.href = "/auth/login?signup=1";
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// ignore transient fetch errors
|
|
94
|
+
}
|
|
95
|
+
await new Promise((res) => setTimeout(res, interval));
|
|
96
|
+
}
|
|
97
|
+
// Timeout: navigate anyway to ensure user sees login
|
|
98
|
+
window.location.href = "/auth/login?signup=1";
|
|
99
|
+
};
|
|
100
|
+
waitForSignOut();
|
|
101
|
+
} else {
|
|
102
|
+
const json = await res.json();
|
|
103
|
+
toast.error(json?.message || "Failed to reset password");
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
toast.error("Failed to reset password");
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (valid === null)
|
|
111
|
+
return (
|
|
112
|
+
<div className="mx-auto w-full max-w-md pt-6">Checking token...</div>
|
|
113
|
+
);
|
|
114
|
+
if (valid === false)
|
|
115
|
+
return (
|
|
116
|
+
<div className="mx-auto w-full max-w-md pt-6">
|
|
117
|
+
Invalid or expired token.
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className="mx-auto w-full max-w-md pt-6">
|
|
123
|
+
<h2 className="text-foreground text-center text-2xl font-bold">
|
|
124
|
+
Reset password
|
|
125
|
+
</h2>
|
|
126
|
+
<p className="text-muted-foreground mt-1 text-center text-sm">
|
|
127
|
+
Set a new password for your account.
|
|
128
|
+
</p>
|
|
129
|
+
|
|
130
|
+
<Form methods={methods}>
|
|
131
|
+
<form
|
|
132
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
133
|
+
className="border-border bg-card space-y-4 rounded-lg border p-6 shadow-sm"
|
|
134
|
+
>
|
|
135
|
+
<FormField
|
|
136
|
+
control={control}
|
|
137
|
+
name="password"
|
|
138
|
+
render={({ field: f }) => (
|
|
139
|
+
<FormItem>
|
|
140
|
+
<FormLabel>New password</FormLabel>
|
|
141
|
+
<FormControl>
|
|
142
|
+
<Input
|
|
143
|
+
id="password"
|
|
144
|
+
type="password"
|
|
145
|
+
placeholder="At least 6 characters"
|
|
146
|
+
{...f}
|
|
147
|
+
/>
|
|
148
|
+
</FormControl>
|
|
149
|
+
<FormMessage />
|
|
150
|
+
</FormItem>
|
|
151
|
+
)}
|
|
152
|
+
/>
|
|
153
|
+
|
|
154
|
+
<FormField
|
|
155
|
+
control={control}
|
|
156
|
+
name="confirmPassword"
|
|
157
|
+
render={({ field: f }) => (
|
|
158
|
+
<FormItem>
|
|
159
|
+
<FormLabel>Confirm password</FormLabel>
|
|
160
|
+
<FormControl>
|
|
161
|
+
<Input
|
|
162
|
+
id="confirmPassword"
|
|
163
|
+
type="password"
|
|
164
|
+
placeholder="Repeat password"
|
|
165
|
+
{...f}
|
|
166
|
+
/>
|
|
167
|
+
</FormControl>
|
|
168
|
+
<FormMessage />
|
|
169
|
+
</FormItem>
|
|
170
|
+
)}
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
<FormField
|
|
174
|
+
control={control}
|
|
175
|
+
name="token"
|
|
176
|
+
render={({ field: f }) => <input type="hidden" {...f} />}
|
|
177
|
+
/>
|
|
178
|
+
|
|
179
|
+
<Button type="submit" disabled={isSubmitting} className="w-full">
|
|
180
|
+
Set password
|
|
181
|
+
</Button>
|
|
182
|
+
</form>
|
|
183
|
+
</Form>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|