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.
Files changed (57) hide show
  1. package/README.md +20 -9
  2. package/dist/kits/auth-core/.nextworks/docs/AUTH_CORE_README.md +3 -3
  3. package/dist/kits/auth-core/.nextworks/docs/AUTH_QUICKSTART.md +264 -244
  4. package/dist/kits/auth-core/app/(protected)/settings/profile/profile-form.tsx +120 -114
  5. package/dist/kits/auth-core/app/api/auth/forgot-password/route.ts +116 -114
  6. package/dist/kits/auth-core/app/api/auth/reset-password/route.ts +66 -63
  7. package/dist/kits/auth-core/app/api/auth/send-verify-email/route.ts +1 -1
  8. package/dist/kits/auth-core/app/api/users/[id]/route.ts +134 -127
  9. package/dist/kits/auth-core/app/auth/reset-password/page.tsx +186 -187
  10. package/dist/kits/auth-core/components/auth/dashboard.tsx +25 -2
  11. package/dist/kits/auth-core/components/auth/forgot-password-form.tsx +90 -90
  12. package/dist/kits/auth-core/components/auth/login-form.tsx +492 -467
  13. package/dist/kits/auth-core/components/auth/signup-form.tsx +28 -29
  14. package/dist/kits/auth-core/lib/auth.ts +46 -15
  15. package/dist/kits/auth-core/lib/forms/map-errors.ts +37 -11
  16. package/dist/kits/auth-core/lib/server/result.ts +45 -45
  17. package/dist/kits/auth-core/lib/validation/forms.ts +1 -2
  18. package/dist/kits/auth-core/package-deps.json +4 -2
  19. package/dist/kits/auth-core/types/next-auth.d.ts +1 -1
  20. package/dist/kits/blocks/.nextworks/docs/BLOCKS_QUICKSTART.md +2 -8
  21. package/dist/kits/blocks/.nextworks/docs/THEME_GUIDE.md +18 -1
  22. package/dist/kits/blocks/app/templates/productlaunch/page.tsx +0 -2
  23. package/dist/kits/blocks/components/sections/FAQ.tsx +0 -1
  24. package/dist/kits/blocks/components/sections/Newsletter.tsx +2 -2
  25. package/dist/kits/blocks/components/ui/switch.tsx +78 -78
  26. package/dist/kits/blocks/components/ui/theme-selector.tsx +1 -1
  27. package/dist/kits/blocks/lib/themes.ts +1 -0
  28. package/dist/kits/blocks/package-deps.json +4 -4
  29. package/dist/kits/data/.nextworks/docs/DATA_QUICKSTART.md +128 -112
  30. package/dist/kits/data/.nextworks/docs/DATA_README.md +2 -1
  31. package/dist/kits/data/app/api/posts/[id]/route.ts +83 -83
  32. package/dist/kits/data/app/api/posts/route.ts +136 -138
  33. package/dist/kits/data/app/api/seed-demo/route.ts +1 -2
  34. package/dist/kits/data/app/api/users/[id]/route.ts +29 -17
  35. package/dist/kits/data/app/api/users/check-email/route.ts +1 -1
  36. package/dist/kits/data/app/api/users/check-unique/route.ts +30 -27
  37. package/dist/kits/data/app/api/users/route.ts +0 -2
  38. package/dist/kits/data/app/examples/demo/create-post-form.tsx +108 -106
  39. package/dist/kits/data/app/examples/demo/page.tsx +2 -1
  40. package/dist/kits/data/app/examples/demo/seed-demo-button.tsx +1 -1
  41. package/dist/kits/data/components/admin/posts-manager.tsx +727 -719
  42. package/dist/kits/data/components/admin/users-manager.tsx +435 -432
  43. package/dist/kits/data/lib/server/result.ts +5 -2
  44. package/dist/kits/data/package-deps.json +1 -1
  45. package/dist/kits/data/scripts/seed-demo.mjs +1 -2
  46. package/dist/kits/forms/app/api/wizard/route.ts +76 -71
  47. package/dist/kits/forms/app/examples/forms/server-action/page.tsx +78 -71
  48. package/dist/kits/forms/components/hooks/useCheckUnique.ts +85 -79
  49. package/dist/kits/forms/components/ui/form/form-control.tsx +28 -28
  50. package/dist/kits/forms/components/ui/form/form-description.tsx +23 -22
  51. package/dist/kits/forms/components/ui/form/form-item.tsx +21 -21
  52. package/dist/kits/forms/components/ui/form/form-label.tsx +24 -24
  53. package/dist/kits/forms/components/ui/form/form-message.tsx +28 -29
  54. package/dist/kits/forms/components/ui/switch.tsx +78 -78
  55. package/dist/kits/forms/lib/forms/map-errors.ts +1 -1
  56. package/dist/kits/forms/lib/validation/forms.ts +1 -2
  57. 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 { userSchema } from "@/lib/validation/forms";
7
- import { Form } from "@/components/ui/form/form";
8
- import { FormField } from "@/components/ui/form/form-field";
9
- import { FormItem } from "@/components/ui/form/form-item";
10
- import { FormLabel } from "@/components/ui/form/form-label";
11
- import { FormControl } from "@/components/ui/form/form-control";
12
- import { FormMessage } from "@/components/ui/form/form-message";
13
- import { Input } from "@/components/ui/input";
14
- import { Button } from "@/components/ui/button";
15
- import { useSession } from "next-auth/react";
16
- import { mapApiErrorsToForm } from "@/lib/forms/map-errors";
17
- import { toast } from "sonner";
18
-
19
- export default function ProfileForm() {
20
- const { data: session, update } = useSession();
21
-
22
- const methods = useForm({
23
- resolver: zodResolver(userSchema),
24
- defaultValues: { name: "", email: "" },
25
- });
26
- const { control, handleSubmit, reset } = methods;
27
- const [isSaving, setIsSaving] = useState(false);
28
-
29
- useEffect(() => {
30
- if (session?.user) {
31
- reset({ name: session.user.name ?? "", email: session.user.email ?? "" });
32
- }
33
- }, [session, reset]);
34
-
35
- const onSubmit = async (values: any) => {
36
- if (!session?.user?.id) {
37
- toast.error("You must be signed in to update your profile");
38
- return;
39
- }
40
-
41
- setIsSaving(true);
42
- try {
43
- const res = await fetch(`/api/users/${session.user.id}`, {
44
- method: "PUT",
45
- headers: { "Content-Type": "application/json" },
46
- body: JSON.stringify(values),
47
- });
48
-
49
- const payload = await res.json();
50
-
51
- const globalMessage = mapApiErrorsToForm(methods, payload);
52
-
53
- if (payload?.success) {
54
- toast.success(payload.message ?? "Profile updated");
55
- // update local form values to reflect saved state
56
- reset(values);
57
- // Patch the NextAuth session so UI reflects the change immediately
58
- await update({ name: values.name, email: values.email });
59
- } else {
60
- if (globalMessage) {
61
- toast.error(globalMessage);
62
- } else {
63
- toast.error(payload?.message ?? "Failed to update profile");
64
- }
65
- }
66
- } catch (e) {
67
- console.error("Failed to update profile", e);
68
- toast.error("Network error while updating profile");
69
- } finally {
70
- setIsSaving(false);
71
- }
72
- };
73
-
74
- return (
75
- <div className="bg-card rounded-md p-6">
76
- <h2 className="mb-3 text-lg font-semibold">Profile settings</h2>
77
- <Form methods={methods}>
78
- <form onSubmit={handleSubmit(onSubmit)} className="space-y-3">
79
- <FormField
80
- control={control}
81
- name="name"
82
- render={({ field }) => (
83
- <FormItem>
84
- <FormLabel>Name</FormLabel>
85
- <FormControl>
86
- <Input {...field} />
87
- </FormControl>
88
- <FormMessage />
89
- </FormItem>
90
- )}
91
- />
92
-
93
- <FormField
94
- control={control}
95
- name="email"
96
- render={({ field }) => (
97
- <FormItem>
98
- <FormLabel>Email</FormLabel>
99
- <FormControl>
100
- <Input {...field} />
101
- </FormControl>
102
- <FormMessage />
103
- </FormItem>
104
- )}
105
- />
106
-
107
- <Button type="submit" disabled={isSaving}>
108
- {isSaving ? "Saving..." : "Save"}
109
- </Button>
110
- </form>
111
- </Form>
112
- </div>
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 { Prisma } from "@prisma/client";
4
- import { prisma } from "@/lib/prisma";
5
- import { forgotPasswordSchema } from "@/lib/validation/forms";
6
- import { randomBytes, createHash } from "crypto";
7
- import { sendDevEmail } from "@/lib/email/dev-transport";
8
- import { sendEmail, isEmailProviderConfigured } from "@/lib/email";
9
-
10
- const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000; // 1 hour
11
- const MAX_PER_WINDOW = 5;
12
- const ipMap = new Map<string, number[]>();
13
-
14
- function rateLimit(ip: string) {
15
- const now = Date.now();
16
- const arr = ipMap.get(ip) ?? [];
17
- const pruned = arr.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
18
- pruned.push(now);
19
- ipMap.set(ip, pruned);
20
- return pruned.length <= MAX_PER_WINDOW;
21
- }
22
-
23
- export async function POST(req: Request) {
24
- if (process.env.NEXTWORKS_ENABLE_PASSWORD_RESET !== "1") {
25
- return NextResponse.json({ message: "Not found" }, { status: 404 });
26
- }
27
- const ip = req.headers.get("x-forwarded-for") || "local";
28
- if (!rateLimit(ip)) {
29
- return NextResponse.json({ message: "Too many requests" }, { status: 429 });
30
- }
31
-
32
- let body: unknown;
33
- try {
34
- body = await req.json();
35
- } catch {
36
- return NextResponse.json({ message: "Invalid request" }, { status: 400 });
37
- }
38
-
39
- try {
40
- const parsed = forgotPasswordSchema.parse(body);
41
- const { email } = parsed;
42
-
43
- const user = await prisma.user.findUnique({ where: { email } });
44
- if (user) {
45
- const token = randomBytes(24).toString("hex");
46
- const tokenHash = createHash("sha256").update(token).digest("hex");
47
- const expires = new Date(Date.now() + 1000 * 60 * 60); // 1 hour
48
- await prisma.passwordReset.create({
49
- data: {
50
- tokenHash,
51
- expires,
52
- user: { connect: { id: user.id } },
53
- },
54
- });
55
-
56
- const mailConfigured = isEmailProviderConfigured();
57
- const isProd = process.env.NODE_ENV === "production";
58
-
59
- if (
60
- isProd &&
61
- process.env.NEXTWORKS_ENABLE_PASSWORD_RESET === "1" &&
62
- !mailConfigured
63
- ) {
64
- return NextResponse.json({ message: "Not found" }, { status: 404 });
65
- }
66
-
67
- try {
68
- const base = process.env.NEXTAUTH_URL ?? "http://localhost:3000";
69
- const resetUrl = `${base.replace(/\/$/, "")}/auth/reset-password?token=${token}`;
70
-
71
- if (mailConfigured) {
72
- try {
73
- await sendEmail({
74
- to: email,
75
- subject: "Password reset",
76
- text: `Reset your password: ${resetUrl}`,
77
- html: `<p>Reset your password: <a href="${resetUrl}">Reset password</a></p>`,
78
- });
79
- console.info(`Password reset email queued for ${email}`);
80
- } catch (e) {
81
- console.error("Failed to send password reset email");
82
- }
83
- } else if (process.env.NEXTWORKS_USE_DEV_EMAIL === "1") {
84
- try {
85
- const { previewUrl } = await sendDevEmail({
86
- to: email,
87
- subject: "Password reset",
88
- text: `Reset your password: ${resetUrl}`,
89
- html: `<p>Reset your password: <a href="${resetUrl}">Reset password</a></p>`,
90
- });
91
- if (previewUrl) {
92
- console.info(`Password reset email (dev preview): ${previewUrl}`);
93
- } else {
94
- console.info(`Password reset email queued for ${email}`);
95
- }
96
- } catch (e) {
97
- console.error("Failed to send dev password reset email");
98
- }
99
- } else {
100
- console.info(`Password reset requested for ${email}`);
101
- }
102
- } catch (e) {
103
- console.error("Failed to handle password reset email");
104
- }
105
- }
106
-
107
- return NextResponse.json({ message: "If an account exists, a reset link was sent." });
108
- } catch (err: any) {
109
- if (err instanceof z.ZodError) {
110
- return NextResponse.json({ message: "Validation failed", errors: (err as z.ZodError).issues }, { status: 400 });
111
- }
112
- return NextResponse.json({ message: "Failed" }, { status: 400 });
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 ((pr as any).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: any) {
41
- if (err instanceof z.ZodError) {
42
- return NextResponse.json({ message: "Validation failed", errors: (err as z.ZodError).issues }, { status: 400 });
43
- }
44
- return NextResponse.json({ message: "Failed" }, { status: 400 });
45
- }
46
- }
47
-
48
- export async function GET(req: Request) {
49
- if (process.env.NEXTWORKS_ENABLE_PASSWORD_RESET !== "1") {
50
- return NextResponse.json({ message: "Not found" }, { status: 404 });
51
- }
52
- const url = new URL(req.url);
53
- const token = url.searchParams.get("token");
54
- if (!token) return NextResponse.json({ valid: false }, { status: 400 });
55
-
56
- const tokenHash = createHash("sha256").update(token).digest("hex");
57
- const pr = await prisma.passwordReset.findFirst({ where: { tokenHash } });
58
- if (!pr) return NextResponse.json({ valid: false });
59
- if ((pr as any).used) return NextResponse.json({ valid: false });
60
- if (pr.expires < new Date()) return NextResponse.json({ valid: false });
61
-
62
- return NextResponse.json({ valid: true });
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(req: Request) {
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
  }