@arch-cadre/auth 1.0.6 → 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 +4 -4
- package/dist/ui/forgot-password/components.mjs +3 -3
- 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 +4 -4
- package/dist/ui/reset-password/components.mjs +3 -3
- 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 +4 -4
- package/dist/ui/reset-password/verify-email/components.mjs +3 -3
- 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 +4 -4
- package/dist/ui/signin/components.d.ts +1 -1
- package/dist/ui/signin/components.mjs +3 -3
- package/dist/ui/signin/page.cjs +4 -4
- package/dist/ui/signin/page.mjs +2 -2
- package/dist/ui/signup/components.cjs +4 -4
- package/dist/ui/signup/components.d.ts +1 -1
- package/dist/ui/signup/components.mjs +3 -3
- package/dist/ui/signup/page.cjs +5 -5
- package/dist/ui/signup/page.mjs +3 -3
- package/dist/ui/verify-email/components.cjs +5 -5
- package/dist/ui/verify-email/components.mjs +3 -3
- 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,230 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useTranslation } from "@arch-cadre/intl";
|
|
3
|
+
import { Button } from "@arch-cadre/ui/components/button";
|
|
4
|
+
import { Checkbox } from "@arch-cadre/ui/components/checkbox";
|
|
5
|
+
import {
|
|
6
|
+
Field,
|
|
7
|
+
FieldError,
|
|
8
|
+
FieldGroup,
|
|
9
|
+
FieldLabel,
|
|
10
|
+
} from "@arch-cadre/ui/components/field";
|
|
11
|
+
import { Input } from "@arch-cadre/ui/components/input";
|
|
12
|
+
import { Separator } from "@arch-cadre/ui/components/separator";
|
|
13
|
+
import { cn } from "@arch-cadre/ui/lib/utils";
|
|
14
|
+
import { Loader } from "@arch-cadre/ui/shared/loader";
|
|
15
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
16
|
+
import { AlertCircle } from "lucide-react";
|
|
17
|
+
import Link from "next/link";
|
|
18
|
+
import * as React from "react";
|
|
19
|
+
import { createContext, useContext, useState } from "react";
|
|
20
|
+
import type { UseFormReturn } from "react-hook-form";
|
|
21
|
+
import { Controller, useForm } from "react-hook-form";
|
|
22
|
+
import { toast } from "sonner";
|
|
23
|
+
import { signupAction } from "../../actions/index.js";
|
|
24
|
+
import { type RegisterInput, registerSchema } from "../../validation.js";
|
|
25
|
+
|
|
26
|
+
const SignUpFormContext = createContext<{
|
|
27
|
+
form: UseFormReturn<RegisterInput>;
|
|
28
|
+
} | null>(null);
|
|
29
|
+
|
|
30
|
+
export function useSignUpForm() {
|
|
31
|
+
const context = useContext(SignUpFormContext);
|
|
32
|
+
if (!context) {
|
|
33
|
+
throw new Error("useSignUpForm must be used within a SignUpForm");
|
|
34
|
+
}
|
|
35
|
+
return context;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function SignUpForm({
|
|
39
|
+
extraButtons,
|
|
40
|
+
extraFields,
|
|
41
|
+
hasAllowedExtraButtons,
|
|
42
|
+
}: {
|
|
43
|
+
extraButtons?: React.ReactNode;
|
|
44
|
+
extraFields?: React.ReactNode;
|
|
45
|
+
hasAllowedExtraButtons?: boolean;
|
|
46
|
+
}) {
|
|
47
|
+
const [generalError, setGeneralError] = useState("");
|
|
48
|
+
const { t } = useTranslation();
|
|
49
|
+
|
|
50
|
+
const form = useForm<RegisterInput>({
|
|
51
|
+
resolver: zodResolver(registerSchema),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
async function onSubmit(data: RegisterInput) {
|
|
55
|
+
setGeneralError("");
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const response = await signupAction(data);
|
|
59
|
+
|
|
60
|
+
if (response.error) {
|
|
61
|
+
setGeneralError(response.message || t("error_occurred"));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
toast(response.message);
|
|
66
|
+
} catch (_error) {
|
|
67
|
+
// setGeneralError(t("error_occurred"));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<SignUpFormContext.Provider value={{ form }}>
|
|
73
|
+
<div className={cn("flex flex-col space-y-6")}>
|
|
74
|
+
<div className="space-y-2 text-center">
|
|
75
|
+
<h1 className="text-3xl font-semibold">{t("Sign Up")}</h1>
|
|
76
|
+
<p className="text-muted-foreground">
|
|
77
|
+
{t("Sign up to access your dashboard, settings and projects.")}
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className="grid gap-5">
|
|
82
|
+
{hasAllowedExtraButtons && (
|
|
83
|
+
<>
|
|
84
|
+
<div className="flex flex-col gap-2">{extraButtons}</div>
|
|
85
|
+
|
|
86
|
+
<div className="flex items-center gap-2">
|
|
87
|
+
<Separator className="flex-1" />
|
|
88
|
+
<span className="text-sm text-muted-foreground">
|
|
89
|
+
{t("or sign up with email")}
|
|
90
|
+
</span>
|
|
91
|
+
<Separator className="flex-1" />
|
|
92
|
+
</div>
|
|
93
|
+
</>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{generalError && (
|
|
97
|
+
<div className="flex gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
|
|
98
|
+
<AlertCircle className="w-4 h-4 text-red-600 flex-shrink-0 mt-0.5" />
|
|
99
|
+
<p className="text-sm text-red-600">{generalError}</p>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6">
|
|
104
|
+
<FieldGroup>
|
|
105
|
+
<Controller
|
|
106
|
+
name="email"
|
|
107
|
+
control={form.control}
|
|
108
|
+
render={({ field, fieldState }) => (
|
|
109
|
+
<Field data-invalid={fieldState.invalid}>
|
|
110
|
+
<FieldLabel htmlFor="email">
|
|
111
|
+
{t("Email address")}
|
|
112
|
+
</FieldLabel>
|
|
113
|
+
<Input
|
|
114
|
+
{...field}
|
|
115
|
+
id="email"
|
|
116
|
+
type="email"
|
|
117
|
+
aria-invalid={fieldState.invalid}
|
|
118
|
+
placeholder={t("Email address")}
|
|
119
|
+
autoComplete="off"
|
|
120
|
+
/>
|
|
121
|
+
{fieldState.invalid && (
|
|
122
|
+
<FieldError errors={[fieldState.error]} />
|
|
123
|
+
)}
|
|
124
|
+
</Field>
|
|
125
|
+
)}
|
|
126
|
+
/>
|
|
127
|
+
<Controller
|
|
128
|
+
name="username"
|
|
129
|
+
control={form.control}
|
|
130
|
+
render={({ field, fieldState }) => (
|
|
131
|
+
<Field data-invalid={fieldState.invalid}>
|
|
132
|
+
<FieldLabel htmlFor="username">{t("Username")}</FieldLabel>
|
|
133
|
+
<Input
|
|
134
|
+
{...field}
|
|
135
|
+
id="username"
|
|
136
|
+
type="text"
|
|
137
|
+
aria-invalid={fieldState.invalid}
|
|
138
|
+
placeholder={t("Username")}
|
|
139
|
+
autoComplete="off"
|
|
140
|
+
/>
|
|
141
|
+
{fieldState.invalid && (
|
|
142
|
+
<FieldError errors={[fieldState.error]} />
|
|
143
|
+
)}
|
|
144
|
+
</Field>
|
|
145
|
+
)}
|
|
146
|
+
/>
|
|
147
|
+
<Controller
|
|
148
|
+
name="password"
|
|
149
|
+
control={form.control}
|
|
150
|
+
render={({ field, fieldState }) => (
|
|
151
|
+
<Field data-invalid={fieldState.invalid}>
|
|
152
|
+
<FieldLabel htmlFor="password">{t("Password")}</FieldLabel>
|
|
153
|
+
<Input
|
|
154
|
+
{...field}
|
|
155
|
+
id="password"
|
|
156
|
+
type="password"
|
|
157
|
+
aria-invalid={fieldState.invalid}
|
|
158
|
+
placeholder={t("Password")}
|
|
159
|
+
autoComplete="off"
|
|
160
|
+
/>
|
|
161
|
+
{fieldState.invalid && (
|
|
162
|
+
<FieldError errors={[fieldState.error]} />
|
|
163
|
+
)}
|
|
164
|
+
</Field>
|
|
165
|
+
)}
|
|
166
|
+
/>
|
|
167
|
+
|
|
168
|
+
{extraFields}
|
|
169
|
+
|
|
170
|
+
<Controller
|
|
171
|
+
name="terms"
|
|
172
|
+
control={form.control}
|
|
173
|
+
render={({ field, fieldState }) => (
|
|
174
|
+
<Field
|
|
175
|
+
data-invalid={fieldState.invalid}
|
|
176
|
+
className="flex items-center space-x-2"
|
|
177
|
+
>
|
|
178
|
+
<div className="flex items-center space-x-2">
|
|
179
|
+
<Checkbox
|
|
180
|
+
id="terms"
|
|
181
|
+
aria-invalid={fieldState.invalid}
|
|
182
|
+
checked={field.value}
|
|
183
|
+
onCheckedChange={(checked) => field.onChange(checked)}
|
|
184
|
+
/>
|
|
185
|
+
<FieldLabel
|
|
186
|
+
htmlFor="terms"
|
|
187
|
+
className="text-sm text-muted-foreground"
|
|
188
|
+
>
|
|
189
|
+
{t("I agree to the")}{" "}
|
|
190
|
+
<Link href="#" className="text-primary hover:underline">
|
|
191
|
+
{t("Terms")}
|
|
192
|
+
</Link>{" "}
|
|
193
|
+
{t("and")}{" "}
|
|
194
|
+
<Link href="#" className="text-primary hover:underline">
|
|
195
|
+
{t("Privacy Policy")}
|
|
196
|
+
</Link>
|
|
197
|
+
</FieldLabel>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{fieldState.invalid && (
|
|
201
|
+
<FieldError errors={[fieldState.error]} />
|
|
202
|
+
)}
|
|
203
|
+
</Field>
|
|
204
|
+
)}
|
|
205
|
+
/>
|
|
206
|
+
</FieldGroup>
|
|
207
|
+
|
|
208
|
+
<Button disabled={form.formState.isSubmitting} className="w-full">
|
|
209
|
+
{form.formState.isSubmitting ? t("Please wait...") : t("Sign Up")}
|
|
210
|
+
{form.formState.isSubmitting && <Loader variant="dark" />}
|
|
211
|
+
</Button>
|
|
212
|
+
|
|
213
|
+
{/* {extraButtons} */}
|
|
214
|
+
</form>
|
|
215
|
+
|
|
216
|
+
<div className="text-center text-sm">
|
|
217
|
+
{t("Already have an account?")}{" "}
|
|
218
|
+
<Link
|
|
219
|
+
key="signin"
|
|
220
|
+
href="/signin"
|
|
221
|
+
className="underline underline-offset-4"
|
|
222
|
+
>
|
|
223
|
+
{t("Sign In")}
|
|
224
|
+
</Link>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</SignUpFormContext.Provider>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getCurrentSession } from "@arch-cadre/core/server";
|
|
2
|
+
import { ExtensionPoint } from "@arch-cadre/modules";
|
|
3
|
+
import { hasExtension } from "@arch-cadre/modules/server";
|
|
4
|
+
import { redirect } from "next/navigation";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { SignUpForm } from "./components.js";
|
|
7
|
+
|
|
8
|
+
export default async function Page() {
|
|
9
|
+
const { session, user } = await getCurrentSession();
|
|
10
|
+
|
|
11
|
+
if (session !== null && user !== null) {
|
|
12
|
+
return redirect("/");
|
|
13
|
+
}
|
|
14
|
+
const hasAllowed = await hasExtension("auth", "signup:extra-buttons");
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<SignUpForm
|
|
18
|
+
hasAllowedExtraButtons={hasAllowed}
|
|
19
|
+
extraButtons={
|
|
20
|
+
<ExtensionPoint
|
|
21
|
+
module="auth"
|
|
22
|
+
point="signup:extra-buttons"
|
|
23
|
+
className="flex flex-col gap-2 mt-4"
|
|
24
|
+
/>
|
|
25
|
+
}
|
|
26
|
+
extraFields={
|
|
27
|
+
<ExtensionPoint
|
|
28
|
+
module="auth"
|
|
29
|
+
point="signup:extra-fields"
|
|
30
|
+
className="flex flex-col gap-4"
|
|
31
|
+
/>
|
|
32
|
+
}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useTranslation } from "@arch-cadre/intl";
|
|
3
|
+
import { Button } from "@arch-cadre/ui/components/button";
|
|
4
|
+
import { Field, FieldError, FieldGroup } from "@arch-cadre/ui/components/field";
|
|
5
|
+
import {
|
|
6
|
+
InputOTP,
|
|
7
|
+
InputOTPGroup,
|
|
8
|
+
InputOTPSlot,
|
|
9
|
+
} from "@arch-cadre/ui/components/input-otp";
|
|
10
|
+
import { cn } from "@arch-cadre/ui/lib/utils";
|
|
11
|
+
import { Loader } from "@arch-cadre/ui/shared/loader";
|
|
12
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
13
|
+
import { AlertCircle } from "lucide-react";
|
|
14
|
+
import React, { useActionState, useEffect, useState } from "react";
|
|
15
|
+
import { Controller, useForm } from "react-hook-form";
|
|
16
|
+
import { toast } from "sonner";
|
|
17
|
+
import {
|
|
18
|
+
resendEmailVerificationCodeAction,
|
|
19
|
+
verifyEmailAction,
|
|
20
|
+
} from "../../actions/index.js";
|
|
21
|
+
import { type VerifyEmailInput, verifyEmailSchema } from "../../validation.js";
|
|
22
|
+
|
|
23
|
+
export function EmailVerificationForm({ email = "" }) {
|
|
24
|
+
const [generalError, setGeneralError] = useState("");
|
|
25
|
+
const { t } = useTranslation();
|
|
26
|
+
|
|
27
|
+
const form = useForm<VerifyEmailInput>({
|
|
28
|
+
resolver: zodResolver(verifyEmailSchema),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function onSubmit(data: VerifyEmailInput) {
|
|
32
|
+
setGeneralError("");
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await verifyEmailAction(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("Verify email")}</h1>
|
|
52
|
+
<p className="text-muted-foreground">
|
|
53
|
+
{t("We've sent a verification code to your email address: {email}", {
|
|
54
|
+
email,
|
|
55
|
+
})}
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div className="grid gap-6">
|
|
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 onSubmit={form.handleSubmit(onSubmit)} className="grid gap-5">
|
|
68
|
+
<FieldGroup className="w-full mx-auto">
|
|
69
|
+
<Controller
|
|
70
|
+
name="code"
|
|
71
|
+
control={form.control}
|
|
72
|
+
render={({ field, fieldState }) => (
|
|
73
|
+
<Field className="w-full" data-invalid={fieldState.invalid}>
|
|
74
|
+
<InputOTP
|
|
75
|
+
{...field}
|
|
76
|
+
className="mx-auto w-full"
|
|
77
|
+
maxLength={6}
|
|
78
|
+
autoFocus={true}
|
|
79
|
+
inputMode="text"
|
|
80
|
+
>
|
|
81
|
+
<InputOTPGroup className="mx-auto">
|
|
82
|
+
<InputOTPSlot index={0} />
|
|
83
|
+
<InputOTPSlot index={1} />
|
|
84
|
+
<InputOTPSlot index={2} />
|
|
85
|
+
<InputOTPSlot index={3} />
|
|
86
|
+
<InputOTPSlot index={4} />
|
|
87
|
+
<InputOTPSlot index={5} />
|
|
88
|
+
</InputOTPGroup>
|
|
89
|
+
</InputOTP>
|
|
90
|
+
{fieldState.invalid && (
|
|
91
|
+
<FieldError errors={[fieldState.error]} />
|
|
92
|
+
)}
|
|
93
|
+
</Field>
|
|
94
|
+
)}
|
|
95
|
+
/>
|
|
96
|
+
</FieldGroup>
|
|
97
|
+
|
|
98
|
+
<Button disabled={form.formState.isSubmitting} className="w-full">
|
|
99
|
+
{form.formState.isSubmitting
|
|
100
|
+
? t("Please wait...")
|
|
101
|
+
: t("Verify Email")}
|
|
102
|
+
{form.formState.isSubmitting && <Loader variant="dark" />}
|
|
103
|
+
</Button>
|
|
104
|
+
</form>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const resendEmailInitialState = {
|
|
111
|
+
message: "",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export function ResendEmailVerificationCodeForm() {
|
|
115
|
+
const { t } = useTranslation();
|
|
116
|
+
const [state, action, isPending] = useActionState(
|
|
117
|
+
resendEmailVerificationCodeAction,
|
|
118
|
+
resendEmailInitialState,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (state.message) {
|
|
123
|
+
toast.success(state.message);
|
|
124
|
+
}
|
|
125
|
+
}, [state]);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<form action={action}>
|
|
129
|
+
<Button disabled={isPending} variant="ghost" className="w-full">
|
|
130
|
+
{isPending ? t("Please wait...") : t("Resend verification email")}
|
|
131
|
+
{isPending && <Loader variant="default" />}
|
|
132
|
+
</Button>
|
|
133
|
+
</form>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCurrentSession,
|
|
3
|
+
getUserEmailVerificationRequestFromRequest,
|
|
4
|
+
} from "@arch-cadre/core/server";
|
|
5
|
+
import { redirect } from "next/navigation";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
EmailVerificationForm,
|
|
10
|
+
ResendEmailVerificationCodeForm,
|
|
11
|
+
} from "./components.js";
|
|
12
|
+
|
|
13
|
+
export default async function Page() {
|
|
14
|
+
const { user } = await getCurrentSession();
|
|
15
|
+
|
|
16
|
+
if (user === null) {
|
|
17
|
+
return redirect("/signin");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// TODO: Ideally we'd sent a new verification email automatically if the previous one is expired,
|
|
21
|
+
// but we can't set cookies inside server components.
|
|
22
|
+
const verificationRequest =
|
|
23
|
+
await getUserEmailVerificationRequestFromRequest();
|
|
24
|
+
|
|
25
|
+
if (verificationRequest === null && user.emailVerifiedAt) {
|
|
26
|
+
return redirect("/?verified");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="space-y-3">
|
|
31
|
+
<EmailVerificationForm email={verificationRequest?.email ?? user.email} />
|
|
32
|
+
|
|
33
|
+
<ResendEmailVerificationCodeForm />
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// Auth validation schemas - CLEAN (No DB dependencies for client-side)
|
|
4
|
+
export const loginSchema = z.object({
|
|
5
|
+
email: z.string().email("Invalid email address"),
|
|
6
|
+
password: z.string().min(8),
|
|
7
|
+
remember: z.boolean().optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const registerSchema = z.object({
|
|
11
|
+
username: z.string().min(2, "Name must be at least 2 characters"),
|
|
12
|
+
email: z.string().email("Invalid email address"),
|
|
13
|
+
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
14
|
+
terms: z.boolean().refine((val) => val === true, "You must accept the terms"),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const forgotPasswordSchema = z.object({
|
|
18
|
+
email: z.string().email("Invalid email address"),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const resetPasswordSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
24
|
+
confirm: z.string(),
|
|
25
|
+
})
|
|
26
|
+
.refine((data) => data.password === data.confirm, {
|
|
27
|
+
message: "Passwords do not match",
|
|
28
|
+
path: ["confirm"],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const verifyEmailSchema = z.object({
|
|
32
|
+
code: z.string().min(6).max(6),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// mfa validation schemas
|
|
36
|
+
export const totpSetupSchema = z.object({
|
|
37
|
+
code: z.string().regex(/^\d{6}$/, "Code must be 6 digits"),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const totpVerifySchema = z.object({
|
|
41
|
+
code: z.string().regex(/^\d{6}$/, "Code must be 6 digits"),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const passkeysSetupSchema = z.object({
|
|
45
|
+
name: z.string().min(1, "Passkey name is required"),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const recoveryCodeVerifySchema = z.object({
|
|
49
|
+
code: z.string().min(16, "Recovery code is required").max(16),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Type exports for use in components
|
|
53
|
+
export type LoginInput = z.infer<typeof loginSchema>;
|
|
54
|
+
export type RegisterInput = z.infer<typeof registerSchema>;
|
|
55
|
+
export type ForgotPasswordInput = z.infer<typeof forgotPasswordSchema>;
|
|
56
|
+
export type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;
|
|
57
|
+
export type TOTPSetupInput = z.infer<typeof totpSetupSchema>;
|
|
58
|
+
export type TOTPVerifyInput = z.infer<typeof totpVerifySchema>;
|
|
59
|
+
export type PasskeysSetupInput = z.infer<typeof passkeysSetupSchema>;
|
|
60
|
+
export type VerifyEmailInput = z.infer<typeof verifyEmailSchema>;
|
|
61
|
+
export type RecoveryVerifyInput = z.infer<typeof recoveryCodeVerifySchema>;
|