@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.
- package/dist/actions/basic.d.ts +1 -1
- package/dist/actions/email.d.ts +1 -1
- package/dist/actions/index.cjs +2 -2
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.mjs +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/routes.cjs +7 -7
- package/dist/routes.mjs +7 -7
- package/dist/ui/forgot-password/components.cjs +2 -2
- package/dist/ui/forgot-password/components.mjs +2 -2
- package/dist/ui/forgot-password/page.cjs +1 -1
- package/dist/ui/forgot-password/page.mjs +1 -1
- package/dist/ui/reset-password/components.cjs +2 -2
- package/dist/ui/reset-password/components.mjs +2 -2
- package/dist/ui/reset-password/page.cjs +1 -1
- package/dist/ui/reset-password/page.mjs +1 -1
- package/dist/ui/reset-password/verify-email/components.cjs +2 -2
- package/dist/ui/reset-password/verify-email/components.mjs +2 -2
- package/dist/ui/reset-password/verify-email/page.cjs +1 -1
- package/dist/ui/reset-password/verify-email/page.mjs +1 -1
- package/dist/ui/signin/components.cjs +2 -2
- package/dist/ui/signin/components.d.ts +1 -1
- package/dist/ui/signin/components.mjs +2 -2
- package/dist/ui/signin/page.cjs +1 -1
- package/dist/ui/signin/page.mjs +1 -1
- package/dist/ui/signup/components.cjs +2 -2
- package/dist/ui/signup/components.d.ts +1 -1
- package/dist/ui/signup/components.mjs +2 -2
- package/dist/ui/signup/page.cjs +1 -1
- package/dist/ui/signup/page.mjs +1 -1
- package/dist/ui/verify-email/components.cjs +2 -2
- package/dist/ui/verify-email/components.mjs +2 -2
- package/dist/ui/verify-email/page.cjs +1 -1
- package/dist/ui/verify-email/page.mjs +1 -1
- package/package.json +8 -7
- package/src/actions/basic.ts +53 -0
- package/src/actions/email.ts +241 -0
- package/src/actions/index.ts +2 -0
- package/src/index.ts +13 -0
- package/src/intl.d.ts +13 -0
- package/src/routes.ts +55 -0
- package/src/types.ts +12 -0
- package/src/ui/forgot-password/components.tsx +101 -0
- package/src/ui/forgot-password/page.tsx +6 -0
- package/src/ui/layout.tsx +42 -0
- package/src/ui/reset-password/components.tsx +128 -0
- package/src/ui/reset-password/page.tsx +25 -0
- package/src/ui/reset-password/verify-email/components.tsx +110 -0
- package/src/ui/reset-password/verify-email/page.tsx +25 -0
- package/src/ui/signin/components.tsx +215 -0
- package/src/ui/signin/page.tsx +43 -0
- package/src/ui/signup/components.tsx +230 -0
- package/src/ui/signup/page.tsx +35 -0
- package/src/ui/verify-email/components.tsx +135 -0
- package/src/ui/verify-email/page.tsx +36 -0
- 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
|
+
}
|
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,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,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
|
+
}
|