@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,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useTranslation } from "@arch-cadre/intl";
|
|
3
|
+
import { Button } from "@arch-cadre/ui/components/button";
|
|
4
|
+
import {
|
|
5
|
+
Field,
|
|
6
|
+
FieldError,
|
|
7
|
+
FieldGroup,
|
|
8
|
+
FieldLabel,
|
|
9
|
+
} from "@arch-cadre/ui/components/field";
|
|
10
|
+
import {
|
|
11
|
+
InputOTP,
|
|
12
|
+
InputOTPGroup,
|
|
13
|
+
InputOTPSlot,
|
|
14
|
+
} from "@arch-cadre/ui/components/input-otp";
|
|
15
|
+
import { cn } from "@arch-cadre/ui/lib/utils";
|
|
16
|
+
import { Loader } from "@arch-cadre/ui/shared/loader";
|
|
17
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
18
|
+
import { AlertCircle } from "lucide-react";
|
|
19
|
+
import * as React from "react";
|
|
20
|
+
import { useState } from "react";
|
|
21
|
+
import { Controller, useForm } from "react-hook-form";
|
|
22
|
+
import { toast } from "sonner";
|
|
23
|
+
import { verifyPasswordResetEmailAction } from "../../../actions/index.js";
|
|
24
|
+
import { type VerifyEmailInput, verifyEmailSchema } from "../../../validation.js";
|
|
25
|
+
|
|
26
|
+
export function PasswordResetEmailVerificationForm({ email = "" }) {
|
|
27
|
+
const [generalError, setGeneralError] = useState("");
|
|
28
|
+
const { t } = useTranslation();
|
|
29
|
+
const form = useForm<VerifyEmailInput>({
|
|
30
|
+
resolver: zodResolver(verifyEmailSchema),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
async function onSubmit(data: VerifyEmailInput) {
|
|
34
|
+
setGeneralError("");
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await verifyPasswordResetEmailAction(data);
|
|
38
|
+
|
|
39
|
+
if (response.error) {
|
|
40
|
+
setGeneralError(
|
|
41
|
+
response.message || t("Verification failed. Please try again."),
|
|
42
|
+
);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
toast(response.message);
|
|
47
|
+
} catch (_error) {
|
|
48
|
+
// setGeneralError(t("error_occurred"));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className={cn("flex flex-col space-y-3")}>
|
|
54
|
+
<div className="space-y-2 text-center">
|
|
55
|
+
<h1 className="text-3xl font-semibold">
|
|
56
|
+
{t("Verify your email address")}
|
|
57
|
+
</h1>
|
|
58
|
+
<p className="text-muted-foreground">
|
|
59
|
+
{t("We sent an 6-digit code to {email}.", { email })}
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div className="grid gap-5">
|
|
64
|
+
{generalError && (
|
|
65
|
+
<div className="flex gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
|
|
66
|
+
<AlertCircle className="w-4 h-4 text-red-600 flex-shrink-0 mt-0.5" />
|
|
67
|
+
<p className="text-sm text-red-600">{generalError}</p>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6">
|
|
72
|
+
<FieldGroup className="w-full mx-auto">
|
|
73
|
+
<Controller
|
|
74
|
+
name="code"
|
|
75
|
+
control={form.control}
|
|
76
|
+
render={({ field, fieldState }) => (
|
|
77
|
+
<Field className="w-full" data-invalid={fieldState.invalid}>
|
|
78
|
+
<InputOTP
|
|
79
|
+
{...field}
|
|
80
|
+
className="mx-auto w-full"
|
|
81
|
+
maxLength={6}
|
|
82
|
+
autoFocus={true}
|
|
83
|
+
inputMode="text"
|
|
84
|
+
>
|
|
85
|
+
<InputOTPGroup className="mx-auto">
|
|
86
|
+
<InputOTPSlot index={0} />
|
|
87
|
+
<InputOTPSlot index={1} />
|
|
88
|
+
<InputOTPSlot index={2} />
|
|
89
|
+
<InputOTPSlot index={3} />
|
|
90
|
+
<InputOTPSlot index={4} />
|
|
91
|
+
<InputOTPSlot index={5} />
|
|
92
|
+
</InputOTPGroup>
|
|
93
|
+
</InputOTP>
|
|
94
|
+
{fieldState.invalid && (
|
|
95
|
+
<FieldError errors={[fieldState.error]} />
|
|
96
|
+
)}
|
|
97
|
+
</Field>
|
|
98
|
+
)}
|
|
99
|
+
/>
|
|
100
|
+
</FieldGroup>
|
|
101
|
+
|
|
102
|
+
<Button disabled={form.formState.isSubmitting} className="w-full">
|
|
103
|
+
{form.formState.isSubmitting ? t("Please wait...") : t("Verify")}
|
|
104
|
+
{form.formState.isSubmitting && <Loader variant="dark" />}
|
|
105
|
+
</Button>
|
|
106
|
+
</form>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -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 { PasswordResetEmailVerificationForm } from "./components.js";
|
|
8
|
+
|
|
9
|
+
export default async function Page() {
|
|
10
|
+
const { session, user } = await getCurrentPasswordResetSession();
|
|
11
|
+
|
|
12
|
+
if (session === null || user === null) {
|
|
13
|
+
return redirect("/forgot-password");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (session.emailVerified) {
|
|
17
|
+
const security = await checkSecurity(session as any, user);
|
|
18
|
+
if (!security.satisfied && security.redirect) {
|
|
19
|
+
return redirect(security.redirect);
|
|
20
|
+
}
|
|
21
|
+
return redirect("/reset-password");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return <PasswordResetEmailVerificationForm email={session.email} />;
|
|
25
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslation } from "@arch-cadre/intl";
|
|
4
|
+
import { Button } from "@arch-cadre/ui/components/button";
|
|
5
|
+
import { Checkbox } from "@arch-cadre/ui/components/checkbox";
|
|
6
|
+
import {
|
|
7
|
+
Field,
|
|
8
|
+
FieldError,
|
|
9
|
+
FieldGroup,
|
|
10
|
+
FieldLabel,
|
|
11
|
+
} from "@arch-cadre/ui/components/field";
|
|
12
|
+
import { Input } from "@arch-cadre/ui/components/input";
|
|
13
|
+
import { Separator } from "@arch-cadre/ui/components/separator";
|
|
14
|
+
import { cn } from "@arch-cadre/ui/lib/utils";
|
|
15
|
+
import { Loader } from "@arch-cadre/ui/shared/loader";
|
|
16
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
17
|
+
import { AlertCircle } from "lucide-react";
|
|
18
|
+
import Link from "next/link";
|
|
19
|
+
import * as React from "react";
|
|
20
|
+
import { createContext, useContext, useState } from "react";
|
|
21
|
+
import type { UseFormReturn } from "react-hook-form";
|
|
22
|
+
import { Controller, useForm } from "react-hook-form";
|
|
23
|
+
import { toast } from "sonner";
|
|
24
|
+
import { loginAction } from "../../actions/index.js";
|
|
25
|
+
import { type LoginInput, loginSchema } from "../../validation.js";
|
|
26
|
+
|
|
27
|
+
const LoginFormContext = createContext<{
|
|
28
|
+
form: UseFormReturn<LoginInput>;
|
|
29
|
+
} | null>(null);
|
|
30
|
+
|
|
31
|
+
export function useLoginForm() {
|
|
32
|
+
const context = useContext(LoginFormContext);
|
|
33
|
+
if (!context) {
|
|
34
|
+
throw new Error("useLoginForm must be used within a LoginForm");
|
|
35
|
+
}
|
|
36
|
+
return context;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function LoginForm({
|
|
40
|
+
extraButtons,
|
|
41
|
+
extraFields,
|
|
42
|
+
hasAllowedExtensions,
|
|
43
|
+
}: {
|
|
44
|
+
hasAllowedExtensions?: boolean;
|
|
45
|
+
extraButtons?: React.ReactNode;
|
|
46
|
+
extraFields?: React.ReactNode;
|
|
47
|
+
}) {
|
|
48
|
+
const [generalError, setGeneralError] = useState("");
|
|
49
|
+
const { t } = useTranslation();
|
|
50
|
+
|
|
51
|
+
const form = useForm<LoginInput>({
|
|
52
|
+
resolver: zodResolver(loginSchema),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
async function onSubmit(data: LoginInput) {
|
|
56
|
+
setGeneralError("");
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await loginAction(data);
|
|
60
|
+
|
|
61
|
+
if (response.error) {
|
|
62
|
+
setGeneralError(response.message || t("error_occurred"));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
toast(response.message);
|
|
67
|
+
} catch (_error) {
|
|
68
|
+
// setGeneralError(t("error_occurred"));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<LoginFormContext.Provider value={{ form }}>
|
|
74
|
+
<div className={cn("flex flex-col space-y-6")}>
|
|
75
|
+
<div className="space-y-2 text-center">
|
|
76
|
+
<h1 className="text-3xl font-semibold">{t("Sign In")}</h1>
|
|
77
|
+
<p className="text-muted-foreground">
|
|
78
|
+
{t("Sign in to access your dashboard, settings and projects.")}
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div className="grid gap-5">
|
|
83
|
+
{hasAllowedExtensions && (
|
|
84
|
+
<>
|
|
85
|
+
<div className="flex flex-col gap-2">{extraButtons}</div>
|
|
86
|
+
|
|
87
|
+
<div className="flex items-center gap-2">
|
|
88
|
+
<Separator className="flex-1" />
|
|
89
|
+
<span className="text-sm text-muted-foreground">
|
|
90
|
+
{t("or sign in with email")}
|
|
91
|
+
</span>
|
|
92
|
+
<Separator className="flex-1" />
|
|
93
|
+
</div>
|
|
94
|
+
</>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{generalError && (
|
|
98
|
+
<div className="flex gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
|
|
99
|
+
<AlertCircle className="w-4 h-4 text-red-600 flex-shrink-0 mt-0.5" />
|
|
100
|
+
<p className="text-sm text-red-600">{generalError}</p>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6">
|
|
105
|
+
<FieldGroup>
|
|
106
|
+
<Controller
|
|
107
|
+
name="email"
|
|
108
|
+
control={form.control}
|
|
109
|
+
render={({ field, fieldState }) => (
|
|
110
|
+
<Field data-invalid={fieldState.invalid}>
|
|
111
|
+
<FieldLabel htmlFor="email">
|
|
112
|
+
{t("Email address")}
|
|
113
|
+
</FieldLabel>
|
|
114
|
+
<Input
|
|
115
|
+
{...field}
|
|
116
|
+
id="email"
|
|
117
|
+
type="email"
|
|
118
|
+
aria-invalid={fieldState.invalid}
|
|
119
|
+
placeholder={t("Email address")}
|
|
120
|
+
autoComplete="off"
|
|
121
|
+
/>
|
|
122
|
+
{fieldState.invalid && (
|
|
123
|
+
<FieldError errors={[fieldState.error]} />
|
|
124
|
+
)}
|
|
125
|
+
</Field>
|
|
126
|
+
)}
|
|
127
|
+
/>
|
|
128
|
+
|
|
129
|
+
<Controller
|
|
130
|
+
name="password"
|
|
131
|
+
control={form.control}
|
|
132
|
+
render={({ field, fieldState }) => (
|
|
133
|
+
<Field data-invalid={fieldState.invalid}>
|
|
134
|
+
<div className="flex items-center">
|
|
135
|
+
<FieldLabel htmlFor="password">
|
|
136
|
+
{t("Password")}
|
|
137
|
+
</FieldLabel>
|
|
138
|
+
|
|
139
|
+
<Link
|
|
140
|
+
href="/forgot-password"
|
|
141
|
+
className="ml-auto text-sm text-primary! underline-offset-4 hover:underline"
|
|
142
|
+
>
|
|
143
|
+
{t("Forgot your password?")}
|
|
144
|
+
</Link>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<Input
|
|
148
|
+
{...field}
|
|
149
|
+
id="password"
|
|
150
|
+
type="password"
|
|
151
|
+
aria-invalid={fieldState.invalid}
|
|
152
|
+
placeholder="********"
|
|
153
|
+
autoComplete="off"
|
|
154
|
+
/>
|
|
155
|
+
{fieldState.invalid && (
|
|
156
|
+
<FieldError errors={[fieldState.error]} />
|
|
157
|
+
)}
|
|
158
|
+
</Field>
|
|
159
|
+
)}
|
|
160
|
+
/>
|
|
161
|
+
|
|
162
|
+
{extraFields}
|
|
163
|
+
|
|
164
|
+
<Controller
|
|
165
|
+
name="remember"
|
|
166
|
+
control={form.control}
|
|
167
|
+
render={({ field, fieldState }) => (
|
|
168
|
+
<Field
|
|
169
|
+
data-invalid={fieldState.invalid}
|
|
170
|
+
className="flex items-center space-x-2"
|
|
171
|
+
>
|
|
172
|
+
<div className="flex items-center space-x-2">
|
|
173
|
+
<Checkbox
|
|
174
|
+
id="remember-me"
|
|
175
|
+
aria-invalid={fieldState.invalid}
|
|
176
|
+
checked={field.value}
|
|
177
|
+
onCheckedChange={(checked) => field.onChange(checked)}
|
|
178
|
+
/>
|
|
179
|
+
<FieldLabel
|
|
180
|
+
htmlFor="remember-me"
|
|
181
|
+
className="text-sm text-muted-foreground"
|
|
182
|
+
>
|
|
183
|
+
{t("Remember me")}
|
|
184
|
+
</FieldLabel>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{fieldState.invalid && (
|
|
188
|
+
<FieldError errors={[fieldState.error]} />
|
|
189
|
+
)}
|
|
190
|
+
</Field>
|
|
191
|
+
)}
|
|
192
|
+
/>
|
|
193
|
+
</FieldGroup>
|
|
194
|
+
|
|
195
|
+
<Button disabled={form.formState.isSubmitting} className="w-full">
|
|
196
|
+
{form.formState.isSubmitting ? t("Please wait...") : t("Sign In")}
|
|
197
|
+
{form.formState.isSubmitting && <Loader variant="dark" />}
|
|
198
|
+
</Button>
|
|
199
|
+
</form>
|
|
200
|
+
|
|
201
|
+
<div className="text-center text-sm">
|
|
202
|
+
{t("No account?")}{" "}
|
|
203
|
+
<Link
|
|
204
|
+
key="signup"
|
|
205
|
+
href="/signup"
|
|
206
|
+
className="underline underline-offset-4"
|
|
207
|
+
>
|
|
208
|
+
{t("Sign Up")}
|
|
209
|
+
</Link>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</LoginFormContext.Provider>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// import { ExtensionPoint } from "@arch-cadre/ui/shared/extension-point";
|
|
2
|
+
import { checkSecurity, getCurrentSession } from "@arch-cadre/core/server";
|
|
3
|
+
import { ExtensionPoint } from "@arch-cadre/modules";
|
|
4
|
+
import { hasExtension } from "@arch-cadre/modules/server";
|
|
5
|
+
import { redirect } from "next/navigation";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { LoginForm } from "./components.js";
|
|
8
|
+
|
|
9
|
+
export default async function Page() {
|
|
10
|
+
const { session, user } = await getCurrentSession();
|
|
11
|
+
|
|
12
|
+
if (session !== null && user !== null) {
|
|
13
|
+
// Check if there are any pending security requirements (like 2FA)
|
|
14
|
+
const security = await checkSecurity(session, user);
|
|
15
|
+
if (!security.satisfied && security.redirect) {
|
|
16
|
+
return redirect(security.redirect);
|
|
17
|
+
}
|
|
18
|
+
return redirect("/");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const hasAllowed = await hasExtension("auth", "signin:extra-buttons");
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<LoginForm
|
|
25
|
+
hasAllowedExtensions={hasAllowed}
|
|
26
|
+
extraButtons={
|
|
27
|
+
|
|
28
|
+
<ExtensionPoint
|
|
29
|
+
module="auth"
|
|
30
|
+
point="signin:extra-buttons"
|
|
31
|
+
className="flex flex-col gap-2"
|
|
32
|
+
/>
|
|
33
|
+
}
|
|
34
|
+
extraFields={
|
|
35
|
+
<ExtensionPoint
|
|
36
|
+
module="auth"
|
|
37
|
+
point="signin:extra-fields"
|
|
38
|
+
className="flex flex-col gap-4"
|
|
39
|
+
/>
|
|
40
|
+
}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -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
|
+
}
|