@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,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
|
+
}
|
|
@@ -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
|
+
}
|