@arch-cadre/auth 1.0.7 → 1.0.8

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/dist/actions/basic.d.ts +1 -1
  2. package/dist/actions/email.d.ts +1 -1
  3. package/dist/actions/index.cjs +2 -2
  4. package/dist/actions/index.d.ts +2 -2
  5. package/dist/actions/index.mjs +2 -2
  6. package/dist/index.cjs +1 -1
  7. package/dist/index.mjs +1 -1
  8. package/dist/routes.cjs +7 -7
  9. package/dist/routes.mjs +7 -7
  10. package/dist/ui/forgot-password/components.cjs +2 -2
  11. package/dist/ui/forgot-password/components.mjs +2 -2
  12. package/dist/ui/forgot-password/page.cjs +1 -1
  13. package/dist/ui/forgot-password/page.mjs +1 -1
  14. package/dist/ui/reset-password/components.cjs +2 -2
  15. package/dist/ui/reset-password/components.mjs +2 -2
  16. package/dist/ui/reset-password/page.cjs +1 -1
  17. package/dist/ui/reset-password/page.mjs +1 -1
  18. package/dist/ui/reset-password/verify-email/components.cjs +2 -2
  19. package/dist/ui/reset-password/verify-email/components.mjs +2 -2
  20. package/dist/ui/reset-password/verify-email/page.cjs +1 -1
  21. package/dist/ui/reset-password/verify-email/page.mjs +1 -1
  22. package/dist/ui/signin/components.cjs +2 -2
  23. package/dist/ui/signin/components.d.ts +1 -1
  24. package/dist/ui/signin/components.mjs +2 -2
  25. package/dist/ui/signin/page.cjs +1 -1
  26. package/dist/ui/signin/page.mjs +1 -1
  27. package/dist/ui/signup/components.cjs +2 -2
  28. package/dist/ui/signup/components.d.ts +1 -1
  29. package/dist/ui/signup/components.mjs +2 -2
  30. package/dist/ui/signup/page.cjs +1 -1
  31. package/dist/ui/signup/page.mjs +1 -1
  32. package/dist/ui/verify-email/components.cjs +2 -2
  33. package/dist/ui/verify-email/components.mjs +2 -2
  34. package/dist/ui/verify-email/page.cjs +1 -1
  35. package/dist/ui/verify-email/page.mjs +1 -1
  36. package/package.json +8 -7
  37. package/src/actions/basic.ts +53 -0
  38. package/src/actions/email.ts +241 -0
  39. package/src/actions/index.ts +2 -0
  40. package/src/index.ts +13 -0
  41. package/src/intl.d.ts +13 -0
  42. package/src/routes.ts +55 -0
  43. package/src/types.ts +12 -0
  44. package/src/ui/forgot-password/components.tsx +101 -0
  45. package/src/ui/forgot-password/page.tsx +6 -0
  46. package/src/ui/layout.tsx +42 -0
  47. package/src/ui/reset-password/components.tsx +128 -0
  48. package/src/ui/reset-password/page.tsx +25 -0
  49. package/src/ui/reset-password/verify-email/components.tsx +110 -0
  50. package/src/ui/reset-password/verify-email/page.tsx +25 -0
  51. package/src/ui/signin/components.tsx +215 -0
  52. package/src/ui/signin/page.tsx +43 -0
  53. package/src/ui/signup/components.tsx +230 -0
  54. package/src/ui/signup/page.tsx +35 -0
  55. package/src/ui/verify-email/components.tsx +135 -0
  56. package/src/ui/verify-email/page.tsx +36 -0
  57. package/src/validation.ts +61 -0
@@ -0,0 +1,241 @@
1
+ "use server";
2
+
3
+ import type {
4
+ ForgotPasswordInput,
5
+ ResetPasswordInput,
6
+ VerifyEmailInput,
7
+ } from "@arch-cadre/core";
8
+ import {
9
+ forgotPasswordSchema,
10
+ resetPasswordSchema,
11
+ verifyEmailSchema,
12
+ } from "@arch-cadre/core";
13
+ import {
14
+ checkSecurity,
15
+ createEmailVerificationRequest,
16
+ createPasswordResetSession,
17
+ createSession,
18
+ deleteEmailVerificationRequestCookie,
19
+ deletePasswordResetSessionTokenCookie,
20
+ deleteUserEmailVerificationRequest,
21
+ eventBus,
22
+ generateSessionToken,
23
+ getCurrentPasswordResetSession,
24
+ getCurrentSession,
25
+ getUserEmailVerificationRequestFromRequest,
26
+ getUserFromEmail,
27
+ invalidateUserPasswordResetSessions,
28
+ invalidateUserSessions,
29
+ runEmailVerificationValidators,
30
+ runPasswordResetValidators,
31
+ sendPasswordResetEmail,
32
+ sendVerificationEmail,
33
+ setEmailVerificationRequestCookie,
34
+ setPasswordResetSessionAsEmailVerified,
35
+ setPasswordResetSessionTokenCookie,
36
+ setSessionTokenCookie,
37
+ updateUserEmailAndSetEmailAsVerified,
38
+ updateUserPassword,
39
+ verifyPasswordStrength,
40
+ } from "@arch-cadre/core/server";
41
+ import { redirect } from "next/navigation";
42
+ import type { ActionResult } from "../types.js";
43
+
44
+ export async function forgotPasswordAction(
45
+ data: ForgotPasswordInput,
46
+ ): Promise<ActionResult> {
47
+ const { email } = await forgotPasswordSchema.parseAsync(data);
48
+ const user = await getUserFromEmail(email);
49
+ if (!user) {
50
+ return { success: false, message: "Not found user with this email" };
51
+ }
52
+
53
+ await invalidateUserPasswordResetSessions(user.id);
54
+ const sessionToken = await generateSessionToken();
55
+ const session = await createPasswordResetSession(
56
+ sessionToken,
57
+ user.id,
58
+ user.email,
59
+ );
60
+
61
+ await sendPasswordResetEmail(session.email, session.code);
62
+ await setPasswordResetSessionTokenCookie(sessionToken, session.expiresAt);
63
+
64
+ await eventBus.publish("auth:password-reset:requested", {
65
+ userId: user.id,
66
+ email: user.email,
67
+ });
68
+
69
+ return redirect("/reset-password/verify-email");
70
+ }
71
+
72
+ export async function verifyPasswordResetEmailAction(
73
+ data: VerifyEmailInput,
74
+ ): Promise<ActionResult> {
75
+ const { session, user } = await getCurrentPasswordResetSession();
76
+ if (!session || !user) {
77
+ return { success: false, message: "Not authenticated" };
78
+ }
79
+
80
+ if (session.emailVerified) {
81
+ return { success: false, message: "Email already verified" };
82
+ }
83
+
84
+ const { code } = verifyEmailSchema.parse(data);
85
+
86
+ if (session.code !== code) {
87
+ return { success: false, message: "Incorrect code" };
88
+ }
89
+
90
+ await setPasswordResetSessionAsEmailVerified(session.id);
91
+
92
+ // Security requirements check (EXTENSIBLE - handles 2FA redirect automatically)
93
+ const security = await checkSecurity(session as any, user);
94
+ if (!security.satisfied && security.redirect) {
95
+ return redirect(security.redirect);
96
+ }
97
+
98
+ return redirect("/reset-password");
99
+ }
100
+
101
+ export async function verifyEmailAction(
102
+ data: VerifyEmailInput,
103
+ ): Promise<ActionResult> {
104
+ const { session, user } = await getCurrentSession();
105
+
106
+ if (!session || !user) {
107
+ return { success: false, message: "Not authenticated" };
108
+ }
109
+
110
+ let verificationRequest = await getUserEmailVerificationRequestFromRequest();
111
+ if (!verificationRequest) {
112
+ return { success: false, message: "Verification request not found" };
113
+ }
114
+
115
+ const { code } = verifyEmailSchema.parse(data);
116
+
117
+ if (Date.now() >= verificationRequest.expiresAt.getTime()) {
118
+ verificationRequest = await createEmailVerificationRequest(
119
+ user.id,
120
+ verificationRequest.email,
121
+ );
122
+ await sendVerificationEmail(
123
+ verificationRequest.email,
124
+ verificationRequest.code,
125
+ );
126
+ await setEmailVerificationRequestCookie(verificationRequest);
127
+ return {
128
+ success: false,
129
+ message:
130
+ "The verification code was expired. We sent another code to your inbox.",
131
+ };
132
+ }
133
+
134
+ if (verificationRequest.code !== code)
135
+ return { success: false, message: "Incorrect code" };
136
+
137
+ // Modular Interception Point
138
+ const interception = await runEmailVerificationValidators(user.id);
139
+ if (interception) {
140
+ if (interception.status === "CHALLENGE_REQUIRED") {
141
+ return redirect(interception.redirect || "/signin");
142
+ }
143
+ if (interception.status === "ERROR") {
144
+ return { success: false, message: interception.message };
145
+ }
146
+ }
147
+
148
+ await deleteUserEmailVerificationRequest(user.id);
149
+ await invalidateUserPasswordResetSessions(user.id);
150
+ await updateUserEmailAndSetEmailAsVerified(
151
+ user.id,
152
+ verificationRequest.email,
153
+ );
154
+ await deleteEmailVerificationRequestCookie();
155
+
156
+ await eventBus.publish("auth:email-verified", {
157
+ userId: user.id,
158
+ email: verificationRequest.email,
159
+ });
160
+
161
+ return redirect("/");
162
+ }
163
+
164
+ export async function resendEmailVerificationCodeAction(): Promise<ActionResult> {
165
+ const { session, user } = await getCurrentSession();
166
+ if (!session || !user) {
167
+ return { success: false, message: "Not authenticated" };
168
+ }
169
+
170
+ let verificationRequest = await getUserEmailVerificationRequestFromRequest();
171
+ if (!verificationRequest) {
172
+ if (user.emailVerifiedAt) {
173
+ return { success: false, message: "Email already verified" };
174
+ }
175
+ verificationRequest = await createEmailVerificationRequest(
176
+ user.id,
177
+ user.email,
178
+ );
179
+ } else {
180
+ verificationRequest = await createEmailVerificationRequest(
181
+ user.id,
182
+ verificationRequest.email,
183
+ );
184
+ }
185
+
186
+ await sendVerificationEmail(
187
+ verificationRequest.email,
188
+ verificationRequest.code,
189
+ );
190
+ await setEmailVerificationRequestCookie(verificationRequest);
191
+
192
+ await eventBus.publish("auth:verification-requested", {
193
+ userId: user.id,
194
+ email: verificationRequest.email,
195
+ });
196
+
197
+ return { success: true, message: "A new code was sent to your inbox." };
198
+ }
199
+
200
+ export async function resetPasswordAction(
201
+ data: ResetPasswordInput,
202
+ ): Promise<ActionResult> {
203
+ const { session: passwordResetSession, user } =
204
+ await getCurrentPasswordResetSession();
205
+ if (!passwordResetSession || !user) {
206
+ return { success: false, message: "Not authenticated" };
207
+ }
208
+ if (!passwordResetSession.emailVerified) {
209
+ return { success: false, message: "Forbidden" };
210
+ }
211
+
212
+ const { password } = resetPasswordSchema.parse(data);
213
+
214
+ if (!(await verifyPasswordStrength(password))) {
215
+ return { success: false, message: "Weak password" };
216
+ }
217
+
218
+ // Modular Interception Point
219
+ const interception = await runPasswordResetValidators(user.id);
220
+ if (interception) {
221
+ if (interception.status === "CHALLENGE_REQUIRED") {
222
+ return redirect(interception.redirect || "/signin");
223
+ }
224
+ if (interception.status === "ERROR") {
225
+ return { success: false, message: interception.message };
226
+ }
227
+ }
228
+
229
+ await invalidateUserPasswordResetSessions(user.id);
230
+ await invalidateUserSessions(user.id);
231
+ await updateUserPassword(user.id, password);
232
+
233
+ const sessionToken = await generateSessionToken();
234
+ const session = await createSession(sessionToken, user.id, {});
235
+ await setSessionTokenCookie(sessionToken, session.expiresAt);
236
+ await deletePasswordResetSessionTokenCookie();
237
+
238
+ await eventBus.publish("auth:password-reset:completed", { userId: user.id });
239
+
240
+ return redirect("/");
241
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./basic.js";
2
+ export * from "./email.js";
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { IModule } from "@arch-cadre/modules";
2
+ import manifest from "../manifest.json";
3
+ import { publicRoutes } from "./routes.js";
4
+
5
+ const authModule: IModule = {
6
+ manifest,
7
+
8
+ routes: {
9
+ public: publicRoutes,
10
+ },
11
+ };
12
+
13
+ export default authModule;
package/src/intl.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type messages from "../locales/en/global.json";
2
+
3
+ type JsonDataType = typeof messages;
4
+
5
+ // declare global {
6
+ // interface IntlMessages extends JsonDataType {}
7
+ // }
8
+
9
+ declare module "@arch-cadre/intl" {
10
+ export interface IntlMessages extends JsonDataType {}
11
+ }
12
+
13
+ export {};
package/src/routes.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { PublicRouteDefinition } from "@arch-cadre/modules";
2
+ import dynamic from "next/dynamic";
3
+ import React from "react";
4
+
5
+ // Layout
6
+ const AuthLayout = dynamic(() => import("./ui/layout.js"));
7
+
8
+ // Pages
9
+ const SignInPage = dynamic(() => import("./ui/signin/page.js"));
10
+ const SignUpPage = dynamic(() => import("./ui/signup/page.js"));
11
+ const ForgotPasswordPage = dynamic(() => import("./ui/forgot-password/page.js"));
12
+ const ResetPasswordPage = dynamic(() => import("./ui/reset-password/page.js"));
13
+ const VerifyEmailPage = dynamic(() => import("./ui/verify-email/page.js"));
14
+ const ResetPasswordVerifyEmailPage = dynamic(
15
+ () => import("./ui/reset-password/verify-email/page.js"),
16
+ );
17
+
18
+ export const publicRoutes: PublicRouteDefinition[] = [
19
+ {
20
+ path: "/signin",
21
+ component: SignInPage,
22
+ layout: AuthLayout,
23
+ auth: false,
24
+ },
25
+ {
26
+ path: "/signup",
27
+ component: SignUpPage,
28
+ layout: AuthLayout,
29
+ auth: false,
30
+ },
31
+ {
32
+ path: "/forgot-password",
33
+ component: ForgotPasswordPage,
34
+ layout: AuthLayout,
35
+ auth: false,
36
+ },
37
+ {
38
+ path: "/reset-password",
39
+ component: ResetPasswordPage,
40
+ layout: AuthLayout,
41
+ auth: false,
42
+ },
43
+ {
44
+ path: "/verify-email",
45
+ component: VerifyEmailPage,
46
+ layout: AuthLayout,
47
+ auth: false,
48
+ },
49
+ {
50
+ path: "/reset-password/verify-email",
51
+ component: ResetPasswordVerifyEmailPage,
52
+ layout: AuthLayout,
53
+ auth: false,
54
+ },
55
+ ];
package/src/types.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ *Action result.
3
+ */
4
+ export interface ActionResult<T = any> {
5
+ success?: boolean;
6
+ error?: boolean;
7
+ message?: string;
8
+ errors?: {
9
+ [K in keyof T]?: string[];
10
+ };
11
+ inputs?: T;
12
+ }
@@ -0,0 +1,101 @@
1
+ /** biome-ignore-all lint/correctness/noUnusedImports: <all> */
2
+ "use client";
3
+
4
+ import { useTranslation } from "@arch-cadre/intl";
5
+ import { Button } from "@arch-cadre/ui/components/button";
6
+ import { Field, FieldError, FieldGroup } from "@arch-cadre/ui/components/field";
7
+ import { Input } from "@arch-cadre/ui/components/input";
8
+ import { Label } from "@arch-cadre/ui/components/label";
9
+ import { cn } from "@arch-cadre/ui/lib/utils";
10
+ import { Loader } from "@arch-cadre/ui/shared/loader";
11
+ import { zodResolver } from "@hookform/resolvers/zod";
12
+ import { AlertCircle } from "lucide-react";
13
+ import * as React from "react";
14
+ import { useState } from "react";
15
+ import { Controller, useForm } from "react-hook-form";
16
+ import { toast } from "sonner";
17
+ import { forgotPasswordAction } from "../../actions/index.js";
18
+ import {
19
+ type ForgotPasswordInput,
20
+ forgotPasswordSchema,
21
+ } from "../../validation.js";
22
+
23
+ export function ForgotPasswordForm() {
24
+ const [generalError, setGeneralError] = useState("");
25
+ const { t } = useTranslation();
26
+
27
+ const form = useForm<ForgotPasswordInput>({
28
+ resolver: zodResolver(forgotPasswordSchema),
29
+ });
30
+
31
+ async function onSubmit(data: ForgotPasswordInput) {
32
+ setGeneralError("");
33
+
34
+ try {
35
+ const response = await forgotPasswordAction(data);
36
+
37
+ if (response.error) {
38
+ setGeneralError(response.message || t("error_occurred"));
39
+ return;
40
+ }
41
+
42
+ toast(response.message);
43
+ } catch (_error) {
44
+ setGeneralError(t("error_occurred"));
45
+ }
46
+ }
47
+
48
+ return (
49
+ <div className={cn("flex flex-col space-y-6")}>
50
+ <div className="space-y-2 text-center">
51
+ <h1 className="text-3xl font-semibold">{t("Forgot Password")}</h1>
52
+ <p className="text-muted-foreground">
53
+ {t(
54
+ "Enter your email address and we'll send you a link to reset your password.",
55
+ )}
56
+ </p>
57
+ </div>
58
+
59
+ <div className="grid gap-5">
60
+ {generalError && (
61
+ <div className="flex gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
62
+ <AlertCircle className="w-4 h-4 text-red-600 flex-shrink-0 mt-0.5" />
63
+ <p className="text-sm text-red-600">{generalError}</p>
64
+ </div>
65
+ )}
66
+
67
+ <form
68
+ onSubmit={form.handleSubmit(onSubmit)}
69
+ className="flex flex-col gap-4"
70
+ >
71
+ <FieldGroup className="w-full mx-auto">
72
+ <Controller
73
+ name="email"
74
+ control={form.control}
75
+ render={({ field, fieldState }) => (
76
+ <Field className="w-full" data-invalid={fieldState.invalid}>
77
+ <Label htmlFor="email">{t("Email address")}</Label>
78
+ <Input
79
+ {...field}
80
+ placeholder="m@example.com"
81
+ autoFocus={true}
82
+ />
83
+ {fieldState.invalid && (
84
+ <FieldError errors={[fieldState.error]} />
85
+ )}
86
+ </Field>
87
+ )}
88
+ />
89
+ </FieldGroup>
90
+
91
+ <Button disabled={form.formState.isSubmitting} className="w-full">
92
+ {form.formState.isSubmitting
93
+ ? t("Please wait...")
94
+ : t("Send reset link")}
95
+ {form.formState.isSubmitting && <Loader variant="dark" />}
96
+ </Button>
97
+ </form>
98
+ </div>
99
+ </div>
100
+ );
101
+ }
@@ -0,0 +1,6 @@
1
+ import * as React from "react";
2
+ import { ForgotPasswordForm } from "./components.js";
3
+
4
+ export default async function Page() {
5
+ return <ForgotPasswordForm />;
6
+ }
@@ -0,0 +1,42 @@
1
+ // import type { Metadata } from "next";import * as React from "react"
2
+
3
+ import { Logo } from "@arch-cadre/ui/brand/logo";
4
+
5
+ import Image from "next/image";
6
+ import Link from "next/link";
7
+ import * as React from "react";
8
+
9
+ // export const metadata: Metadata = {
10
+ // title: "Auth App",
11
+ // description: "Generated by create next app",
12
+ // };
13
+
14
+ export default function RootLayout({
15
+ children,
16
+ }: Readonly<{
17
+ children: React.ReactNode;
18
+ }>) {
19
+ return (
20
+ <div className="grid min-h-svh grid-cols-1 lg:grid-cols-2">
21
+ <div className="flex flex-col gap-4 p-6 md:p-10">
22
+ <div className="flex justify-center gap-2">
23
+ <Link href="/" className="flex items-center gap-2 font-medium">
24
+ <Logo />
25
+ </Link>
26
+ </div>
27
+ <div className="flex flex-1 items-center justify-center">
28
+ <div className="w-full max-w-xs">{children}</div>
29
+ </div>
30
+ </div>
31
+ <div className="bg-muted relative hidden lg:block">
32
+ <Image
33
+ width={100}
34
+ height={100}
35
+ src="/placeholder.svg"
36
+ alt="Image"
37
+ className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
38
+ />
39
+ </div>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,128 @@
1
+ /** biome-ignore-all lint/correctness/noUnusedImports: <all> */
2
+ "use client";
3
+
4
+ import { useTranslation } from "@arch-cadre/intl";
5
+ import { Button } from "@arch-cadre/ui/components/button";
6
+ import {
7
+ CardDescription,
8
+ CardHeader,
9
+ CardTitle,
10
+ } from "@arch-cadre/ui/components/card";
11
+ import { Checkbox } from "@arch-cadre/ui/components/checkbox";
12
+ import {
13
+ Field,
14
+ FieldError,
15
+ FieldGroup,
16
+ FieldLabel,
17
+ } from "@arch-cadre/ui/components/field";
18
+ import { Input } from "@arch-cadre/ui/components/input";
19
+ import { Label } from "@arch-cadre/ui/components/label";
20
+ import { Separator } from "@arch-cadre/ui/components/separator";
21
+ import { cn } from "@arch-cadre/ui/lib/utils";
22
+ import { Loader } from "@arch-cadre/ui/shared/loader";
23
+ import { zodResolver } from "@hookform/resolvers/zod";
24
+ import { AlertCircle } from "lucide-react";
25
+ import * as React from "react";
26
+ import { useState } from "react";
27
+ import { Controller, useForm } from "react-hook-form";
28
+ import { toast } from "sonner";
29
+ import { resetPasswordAction } from "../../actions/index.js";
30
+ import { type ResetPasswordInput, resetPasswordSchema } from "../../validation.js";
31
+
32
+ export function ResetPasswordForm() {
33
+ const [generalError, setGeneralError] = useState("");
34
+ const { t } = useTranslation();
35
+
36
+ const form = useForm<ResetPasswordInput>({
37
+ resolver: zodResolver(resetPasswordSchema),
38
+ });
39
+
40
+ async function onSubmit(data: ResetPasswordInput) {
41
+ setGeneralError("");
42
+
43
+ try {
44
+ const response = await resetPasswordAction(data);
45
+
46
+ if (response.error) {
47
+ setGeneralError(response.message || t("error_occurred"));
48
+ return;
49
+ }
50
+
51
+ toast(response.message);
52
+ } catch (_error) {
53
+ // setGeneralError(t("error_occurred"));
54
+ }
55
+ }
56
+
57
+ return (
58
+ <div className={cn("flex flex-col space-y-3")}>
59
+ <CardHeader className="text-center">
60
+ <CardTitle className="text-xl">{t("Reset password")}</CardTitle>
61
+ <CardDescription>{t("Enter your new password below.")}</CardDescription>
62
+ </CardHeader>
63
+
64
+ <div className="flex flex-col gap-6">
65
+ {generalError && (
66
+ <div className="flex gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
67
+ <AlertCircle className="w-4 h-4 text-red-600 flex-shrink-0 mt-0.5" />
68
+ <p className="text-sm text-red-600">{generalError}</p>
69
+ </div>
70
+ )}
71
+
72
+ <form
73
+ onSubmit={form.handleSubmit(onSubmit)}
74
+ className="flex flex-col gap-4"
75
+ >
76
+ <FieldGroup className="w-full mx-auto">
77
+ <Controller
78
+ name="password"
79
+ control={form.control}
80
+ render={({ field, fieldState }) => (
81
+ <Field className="w-full" data-invalid={fieldState.invalid}>
82
+ <Label htmlFor="password">{t("New Password")}</Label>
83
+ <Input
84
+ {...field}
85
+ id="password"
86
+ type="password"
87
+ placeholder="********"
88
+ autoFocus={true}
89
+ />
90
+
91
+ {fieldState.invalid && (
92
+ <FieldError errors={[fieldState.error]} />
93
+ )}
94
+ </Field>
95
+ )}
96
+ />
97
+ <Controller
98
+ name="confirm"
99
+ control={form.control}
100
+ render={({ field, fieldState }) => (
101
+ <Field className="w-full" data-invalid={fieldState.invalid}>
102
+ <Label htmlFor="confirm">{t("Confirm Password")}</Label>
103
+ <Input
104
+ {...field}
105
+ id="confirm"
106
+ type="password"
107
+ placeholder="********"
108
+ />
109
+
110
+ {fieldState.invalid && (
111
+ <FieldError errors={[fieldState.error]} />
112
+ )}
113
+ </Field>
114
+ )}
115
+ />
116
+ </FieldGroup>
117
+
118
+ <Button disabled={form.formState.isSubmitting} className="w-full">
119
+ {form.formState.isSubmitting
120
+ ? t("Please wait...")
121
+ : t("Reset password")}
122
+ {form.formState.isSubmitting && <Loader variant="dark" />}{" "}
123
+ </Button>
124
+ </form>
125
+ </div>
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,25 @@
1
+ import {
2
+ checkSecurity,
3
+ getCurrentPasswordResetSession,
4
+ } from "@arch-cadre/core/server";
5
+ import { redirect } from "next/navigation";
6
+ import * as React from "react";
7
+ import { ResetPasswordForm } from "./components.js";
8
+
9
+ export default async function Page() {
10
+ const { session, user } = await getCurrentPasswordResetSession();
11
+ if (session === null || user === null) {
12
+ return redirect("/forgot-password");
13
+ }
14
+ if (!session.emailVerified) {
15
+ return redirect("/reset-password/verify-email");
16
+ }
17
+
18
+ // Security requirements check (for 2FA during reset)
19
+ const security = await checkSecurity(session as any, user);
20
+ if (!security.satisfied && security.redirect) {
21
+ return redirect(security.redirect);
22
+ }
23
+
24
+ return <ResetPasswordForm />;
25
+ }