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,114 +1,120 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useEffect, useState } from "react";
|
|
4
|
-
import { useForm } from "react-hook-form";
|
|
5
|
-
import { zodResolver } from "@hookform/resolvers/zod";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { useForm } from "react-hook-form";
|
|
5
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { userSchema } from "@/lib/validation/forms";
|
|
8
|
+
|
|
9
|
+
import { Form } from "@/components/ui/form/form";
|
|
10
|
+
import { FormField } from "@/components/ui/form/form-field";
|
|
11
|
+
import { FormItem } from "@/components/ui/form/form-item";
|
|
12
|
+
import { FormLabel } from "@/components/ui/form/form-label";
|
|
13
|
+
import { FormControl } from "@/components/ui/form/form-control";
|
|
14
|
+
import { FormMessage } from "@/components/ui/form/form-message";
|
|
15
|
+
import { Input } from "@/components/ui/input";
|
|
16
|
+
import { Button } from "@/components/ui/button";
|
|
17
|
+
import { useSession } from "next-auth/react";
|
|
18
|
+
import { mapApiErrorsToForm } from "@/lib/forms/map-errors";
|
|
19
|
+
import { toast } from "sonner";
|
|
20
|
+
|
|
21
|
+
export default function ProfileForm() {
|
|
22
|
+
const { data: session, update } = useSession();
|
|
23
|
+
|
|
24
|
+
const methods = useForm({
|
|
25
|
+
resolver: zodResolver(userSchema),
|
|
26
|
+
defaultValues: { name: "", email: "" },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
type ProfileFormValues = z.infer<typeof userSchema>;
|
|
30
|
+
|
|
31
|
+
const { control, handleSubmit, reset } = methods;
|
|
32
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (session?.user) {
|
|
36
|
+
reset({ name: session.user.name ?? "", email: session.user.email ?? "" });
|
|
37
|
+
}
|
|
38
|
+
}, [session, reset]);
|
|
39
|
+
|
|
40
|
+
const onSubmit = async (values: ProfileFormValues) => {
|
|
41
|
+
|
|
42
|
+
if (!session?.user?.id) {
|
|
43
|
+
toast.error("You must be signed in to update your profile");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setIsSaving(true);
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(`/api/users/${session.user.id}`, {
|
|
50
|
+
method: "PUT",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify(values),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const payload = await res.json();
|
|
56
|
+
|
|
57
|
+
const globalMessage = mapApiErrorsToForm(methods, payload);
|
|
58
|
+
|
|
59
|
+
if (payload?.success) {
|
|
60
|
+
toast.success(payload.message ?? "Profile updated");
|
|
61
|
+
// update local form values to reflect saved state
|
|
62
|
+
reset(values);
|
|
63
|
+
// Patch the NextAuth session so UI reflects the change immediately
|
|
64
|
+
await update({ name: values.name, email: values.email });
|
|
65
|
+
} else {
|
|
66
|
+
if (globalMessage) {
|
|
67
|
+
toast.error(globalMessage);
|
|
68
|
+
} else {
|
|
69
|
+
toast.error(payload?.message ?? "Failed to update profile");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error("Failed to update profile", e);
|
|
74
|
+
toast.error("Network error while updating profile");
|
|
75
|
+
} finally {
|
|
76
|
+
setIsSaving(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="bg-card rounded-md p-6">
|
|
82
|
+
<h2 className="mb-3 text-lg font-semibold">Profile settings</h2>
|
|
83
|
+
<Form methods={methods}>
|
|
84
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-3">
|
|
85
|
+
<FormField
|
|
86
|
+
control={control}
|
|
87
|
+
name="name"
|
|
88
|
+
render={({ field }) => (
|
|
89
|
+
<FormItem>
|
|
90
|
+
<FormLabel>Name</FormLabel>
|
|
91
|
+
<FormControl>
|
|
92
|
+
<Input {...field} />
|
|
93
|
+
</FormControl>
|
|
94
|
+
<FormMessage />
|
|
95
|
+
</FormItem>
|
|
96
|
+
)}
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
<FormField
|
|
100
|
+
control={control}
|
|
101
|
+
name="email"
|
|
102
|
+
render={({ field }) => (
|
|
103
|
+
<FormItem>
|
|
104
|
+
<FormLabel>Email</FormLabel>
|
|
105
|
+
<FormControl>
|
|
106
|
+
<Input {...field} />
|
|
107
|
+
</FormControl>
|
|
108
|
+
<FormMessage />
|
|
109
|
+
</FormItem>
|
|
110
|
+
)}
|
|
111
|
+
/>
|
|
112
|
+
|
|
113
|
+
<Button type="submit" disabled={isSaving}>
|
|
114
|
+
{isSaving ? "Saving..." : "Save"}
|
|
115
|
+
</Button>
|
|
116
|
+
</form>
|
|
117
|
+
</Form>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -1,114 +1,116 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { forgotPasswordSchema } from "@/lib/validation/forms";
|
|
5
|
+
import { randomBytes, createHash } from "crypto";
|
|
6
|
+
import { sendDevEmail } from "@/lib/email/dev-transport";
|
|
7
|
+
import { sendEmail, isEmailProviderConfigured } from "@/lib/email";
|
|
8
|
+
|
|
9
|
+
const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000; // 1 hour
|
|
10
|
+
const MAX_PER_WINDOW = 5;
|
|
11
|
+
const ipMap = new Map<string, number[]>();
|
|
12
|
+
|
|
13
|
+
function rateLimit(ip: string) {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
const arr = ipMap.get(ip) ?? [];
|
|
16
|
+
const pruned = arr.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
|
|
17
|
+
pruned.push(now);
|
|
18
|
+
ipMap.set(ip, pruned);
|
|
19
|
+
return pruned.length <= MAX_PER_WINDOW;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function POST(req: Request) {
|
|
23
|
+
if (process.env.NEXTWORKS_ENABLE_PASSWORD_RESET !== "1") {
|
|
24
|
+
return NextResponse.json({ message: "Not found" }, { status: 404 });
|
|
25
|
+
}
|
|
26
|
+
const ip = req.headers.get("x-forwarded-for") || "local";
|
|
27
|
+
if (!rateLimit(ip)) {
|
|
28
|
+
return NextResponse.json({ message: "Too many requests" }, { status: 429 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let body: unknown;
|
|
32
|
+
try {
|
|
33
|
+
body = await req.json();
|
|
34
|
+
} catch {
|
|
35
|
+
return NextResponse.json({ message: "Invalid request" }, { status: 400 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const parsed = forgotPasswordSchema.parse(body);
|
|
40
|
+
const { email } = parsed;
|
|
41
|
+
|
|
42
|
+
const user = await prisma.user.findUnique({ where: { email } });
|
|
43
|
+
if (user) {
|
|
44
|
+
const token = randomBytes(24).toString("hex");
|
|
45
|
+
const tokenHash = createHash("sha256").update(token).digest("hex");
|
|
46
|
+
const expires = new Date(Date.now() + 1000 * 60 * 60); // 1 hour
|
|
47
|
+
await prisma.passwordReset.create({
|
|
48
|
+
data: {
|
|
49
|
+
tokenHash,
|
|
50
|
+
expires,
|
|
51
|
+
user: { connect: { id: user.id } },
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const mailConfigured = isEmailProviderConfigured();
|
|
56
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
isProd &&
|
|
60
|
+
process.env.NEXTWORKS_ENABLE_PASSWORD_RESET === "1" &&
|
|
61
|
+
!mailConfigured
|
|
62
|
+
) {
|
|
63
|
+
return NextResponse.json({ message: "Not found" }, { status: 404 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const base = process.env.NEXTAUTH_URL ?? "http://localhost:3000";
|
|
68
|
+
const resetUrl = `${base.replace(/\/$/, "")}/auth/reset-password?token=${token}`;
|
|
69
|
+
|
|
70
|
+
if (mailConfigured) {
|
|
71
|
+
try {
|
|
72
|
+
await sendEmail({
|
|
73
|
+
to: email,
|
|
74
|
+
subject: "Password reset",
|
|
75
|
+
text: `Reset your password: ${resetUrl}`,
|
|
76
|
+
html: `<p>Reset your password: <a href="${resetUrl}">Reset password</a></p>`,
|
|
77
|
+
});
|
|
78
|
+
console.info(`Password reset email queued for ${email}`);
|
|
79
|
+
} catch {
|
|
80
|
+
console.error("Failed to send password reset email");
|
|
81
|
+
}
|
|
82
|
+
} else if (process.env.NEXTWORKS_USE_DEV_EMAIL === "1") {
|
|
83
|
+
try {
|
|
84
|
+
const { previewUrl } = await sendDevEmail({
|
|
85
|
+
to: email,
|
|
86
|
+
subject: "Password reset",
|
|
87
|
+
text: `Reset your password: ${resetUrl}`,
|
|
88
|
+
html: `<p>Reset your password: <a href="${resetUrl}">Reset password</a></p>`,
|
|
89
|
+
});
|
|
90
|
+
if (previewUrl) {
|
|
91
|
+
console.info(`Password reset email (dev preview): ${previewUrl}`);
|
|
92
|
+
} else {
|
|
93
|
+
console.info(`Password reset email queued for ${email}`);
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
console.error("Failed to send dev password reset email");
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
console.info(`Password reset requested for ${email}`);
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
console.error("Failed to handle password reset email");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return NextResponse.json({ message: "If an account exists, a reset link was sent." });
|
|
107
|
+
} catch (err: unknown) {
|
|
108
|
+
if (err instanceof z.ZodError) {
|
|
109
|
+
return NextResponse.json(
|
|
110
|
+
{ message: "Validation failed", errors: err.issues },
|
|
111
|
+
{ status: 400 },
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return NextResponse.json({ message: "Failed" }, { status: 400 });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -1,63 +1,66 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import { prisma } from "@/lib/prisma";
|
|
3
|
-
import { resetPasswordSchema } from "@/lib/validation/forms";
|
|
4
|
-
import { hash } from "bcryptjs";
|
|
5
|
-
import { createHash } from "crypto";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
|
|
8
|
-
export async function POST(req: Request) {
|
|
9
|
-
if (process.env.NEXTWORKS_ENABLE_PASSWORD_RESET !== "1") {
|
|
10
|
-
return NextResponse.json({ message: "Not found" }, { status: 404 });
|
|
11
|
-
}
|
|
12
|
-
let body: unknown;
|
|
13
|
-
try {
|
|
14
|
-
body = await req.json();
|
|
15
|
-
} catch {
|
|
16
|
-
return NextResponse.json({ message: "Invalid request" }, { status: 400 });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const parsed = resetPasswordSchema.parse(body);
|
|
21
|
-
const { token, password } = parsed;
|
|
22
|
-
|
|
23
|
-
const tokenHash = createHash("sha256").update(token).digest("hex");
|
|
24
|
-
const pr = await prisma.passwordReset.findFirst({ where: { tokenHash } });
|
|
25
|
-
if (!pr) {
|
|
26
|
-
return NextResponse.json({ message: "Invalid or expired token" }, { status: 400 });
|
|
27
|
-
}
|
|
28
|
-
if (
|
|
29
|
-
return NextResponse.json({ message: "Token already used" }, { status: 400 });
|
|
30
|
-
}
|
|
31
|
-
if (pr.expires < new Date()) {
|
|
32
|
-
return NextResponse.json({ message: "Token expired" }, { status: 400 });
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const hashed = await hash(password, 10);
|
|
36
|
-
await prisma.user.update({ where: { id: pr.userId }, data: { password: hashed } });
|
|
37
|
-
await prisma.passwordReset.update({ where: { id: pr.id }, data: { used: true } });
|
|
38
|
-
|
|
39
|
-
return NextResponse.json({ message: "Password updated" });
|
|
40
|
-
} catch (err:
|
|
41
|
-
if (err instanceof z.ZodError) {
|
|
42
|
-
return NextResponse.json(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return NextResponse.json({ valid:
|
|
63
|
-
}
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { prisma } from "@/lib/prisma";
|
|
3
|
+
import { resetPasswordSchema } from "@/lib/validation/forms";
|
|
4
|
+
import { hash } from "bcryptjs";
|
|
5
|
+
import { createHash } from "crypto";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
export async function POST(req: Request) {
|
|
9
|
+
if (process.env.NEXTWORKS_ENABLE_PASSWORD_RESET !== "1") {
|
|
10
|
+
return NextResponse.json({ message: "Not found" }, { status: 404 });
|
|
11
|
+
}
|
|
12
|
+
let body: unknown;
|
|
13
|
+
try {
|
|
14
|
+
body = await req.json();
|
|
15
|
+
} catch {
|
|
16
|
+
return NextResponse.json({ message: "Invalid request" }, { status: 400 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const parsed = resetPasswordSchema.parse(body);
|
|
21
|
+
const { token, password } = parsed;
|
|
22
|
+
|
|
23
|
+
const tokenHash = createHash("sha256").update(token).digest("hex");
|
|
24
|
+
const pr = await prisma.passwordReset.findFirst({ where: { tokenHash } });
|
|
25
|
+
if (!pr) {
|
|
26
|
+
return NextResponse.json({ message: "Invalid or expired token" }, { status: 400 });
|
|
27
|
+
}
|
|
28
|
+
if (pr.used) {
|
|
29
|
+
return NextResponse.json({ message: "Token already used" }, { status: 400 });
|
|
30
|
+
}
|
|
31
|
+
if (pr.expires < new Date()) {
|
|
32
|
+
return NextResponse.json({ message: "Token expired" }, { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const hashed = await hash(password, 10);
|
|
36
|
+
await prisma.user.update({ where: { id: pr.userId }, data: { password: hashed } });
|
|
37
|
+
await prisma.passwordReset.update({ where: { id: pr.id }, data: { used: true } });
|
|
38
|
+
|
|
39
|
+
return NextResponse.json({ message: "Password updated" });
|
|
40
|
+
} catch (err: unknown) {
|
|
41
|
+
if (err instanceof z.ZodError) {
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{ message: "Validation failed", errors: err.issues },
|
|
44
|
+
{ status: 400 },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return NextResponse.json({ message: "Failed" }, { status: 400 });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function GET(req: Request) {
|
|
52
|
+
if (process.env.NEXTWORKS_ENABLE_PASSWORD_RESET !== "1") {
|
|
53
|
+
return NextResponse.json({ message: "Not found" }, { status: 404 });
|
|
54
|
+
}
|
|
55
|
+
const url = new URL(req.url);
|
|
56
|
+
const token = url.searchParams.get("token");
|
|
57
|
+
if (!token) return NextResponse.json({ valid: false }, { status: 400 });
|
|
58
|
+
|
|
59
|
+
const tokenHash = createHash("sha256").update(token).digest("hex");
|
|
60
|
+
const pr = await prisma.passwordReset.findFirst({ where: { tokenHash } });
|
|
61
|
+
if (!pr) return NextResponse.json({ valid: false });
|
|
62
|
+
if (pr.used) return NextResponse.json({ valid: false });
|
|
63
|
+
if (pr.expires < new Date()) return NextResponse.json({ valid: false });
|
|
64
|
+
|
|
65
|
+
return NextResponse.json({ valid: true });
|
|
66
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
|
|
3
|
-
export async function POST(
|
|
3
|
+
export async function POST(): Promise<Response> {
|
|
4
4
|
// Placeholder endpoint: future work may implement email verification tokens.
|
|
5
5
|
return NextResponse.json({ message: "Not implemented" }, { status: 501 });
|
|
6
6
|
}
|