@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.
Files changed (57) hide show
  1. package/dist/actions/basic.d.ts +1 -1
  2. package/dist/actions/email.d.ts +1 -1
  3. package/dist/actions/index.cjs +2 -2
  4. package/dist/actions/index.d.ts +2 -2
  5. package/dist/actions/index.mjs +2 -2
  6. package/dist/index.cjs +1 -1
  7. package/dist/index.mjs +1 -1
  8. package/dist/routes.cjs +7 -7
  9. package/dist/routes.mjs +7 -7
  10. package/dist/ui/forgot-password/components.cjs +4 -4
  11. package/dist/ui/forgot-password/components.mjs +3 -3
  12. package/dist/ui/forgot-password/page.cjs +1 -1
  13. package/dist/ui/forgot-password/page.mjs +1 -1
  14. package/dist/ui/reset-password/components.cjs +4 -4
  15. package/dist/ui/reset-password/components.mjs +3 -3
  16. package/dist/ui/reset-password/page.cjs +1 -1
  17. package/dist/ui/reset-password/page.mjs +1 -1
  18. package/dist/ui/reset-password/verify-email/components.cjs +4 -4
  19. package/dist/ui/reset-password/verify-email/components.mjs +3 -3
  20. package/dist/ui/reset-password/verify-email/page.cjs +1 -1
  21. package/dist/ui/reset-password/verify-email/page.mjs +1 -1
  22. package/dist/ui/signin/components.cjs +4 -4
  23. package/dist/ui/signin/components.d.ts +1 -1
  24. package/dist/ui/signin/components.mjs +3 -3
  25. package/dist/ui/signin/page.cjs +4 -4
  26. package/dist/ui/signin/page.mjs +2 -2
  27. package/dist/ui/signup/components.cjs +4 -4
  28. package/dist/ui/signup/components.d.ts +1 -1
  29. package/dist/ui/signup/components.mjs +3 -3
  30. package/dist/ui/signup/page.cjs +5 -5
  31. package/dist/ui/signup/page.mjs +3 -3
  32. package/dist/ui/verify-email/components.cjs +5 -5
  33. package/dist/ui/verify-email/components.mjs +3 -3
  34. package/dist/ui/verify-email/page.cjs +1 -1
  35. package/dist/ui/verify-email/page.mjs +1 -1
  36. package/package.json +8 -7
  37. package/src/actions/basic.ts +53 -0
  38. package/src/actions/email.ts +241 -0
  39. package/src/actions/index.ts +2 -0
  40. package/src/index.ts +13 -0
  41. package/src/intl.d.ts +13 -0
  42. package/src/routes.ts +55 -0
  43. package/src/types.ts +12 -0
  44. package/src/ui/forgot-password/components.tsx +101 -0
  45. package/src/ui/forgot-password/page.tsx +6 -0
  46. package/src/ui/layout.tsx +42 -0
  47. package/src/ui/reset-password/components.tsx +128 -0
  48. package/src/ui/reset-password/page.tsx +25 -0
  49. package/src/ui/reset-password/verify-email/components.tsx +110 -0
  50. package/src/ui/reset-password/verify-email/page.tsx +25 -0
  51. package/src/ui/signin/components.tsx +215 -0
  52. package/src/ui/signin/page.tsx +43 -0
  53. package/src/ui/signup/components.tsx +230 -0
  54. package/src/ui/signup/page.tsx +35 -0
  55. package/src/ui/verify-email/components.tsx +135 -0
  56. package/src/ui/verify-email/page.tsx +36 -0
  57. package/src/validation.ts +61 -0
@@ -0,0 +1,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
+ }