@sqrzro/auth 4.0.0-alpha.8 → 4.0.0-alpha.9
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/.turbo/turbo-dev.log +22 -38
- package/dist/components/Password/index.d.ts +2 -2
- package/dist/components/Password/index.js +1 -1
- package/dist/components/PasswordComplexityFormField/index.d.ts +8 -0
- package/dist/components/PasswordComplexityFormField/index.js +10 -0
- package/dist/components/PasswordComplexityInput/index.d.ts +5 -0
- package/dist/components/PasswordComplexityInput/index.js +9 -0
- package/dist/extend-repository.d.ts +0 -0
- package/dist/extend-repository.js +1 -0
- package/dist/forms/LoginForm/server.js +2 -2
- package/dist/forms/PasswordForm/index.d.ts +6 -1
- package/dist/forms/PasswordForm/index.js +19 -4
- package/dist/forms/PasswordForm/server.js +9 -6
- package/dist/forms/PasswordResetForm/index.js +5 -3
- package/dist/forms/PasswordResetForm/server.js +9 -6
- package/dist/mail/PasswordMail.d.ts +5 -0
- package/dist/mail/PasswordMail.js +6 -0
- package/dist/rules/complexity.d.ts +4 -0
- package/dist/rules/complexity.js +9 -0
- package/dist/rules/password.d.ts +3 -0
- package/dist/rules/password.js +6 -0
- package/dist/utility/create-complexity-schema.d.ts +4 -0
- package/dist/utility/create-complexity-schema.js +22 -0
- package/dist/utility/create-password-schema.d.ts +0 -0
- package/dist/utility/create-password-schema.js +1 -0
- package/dist/utility/get-complexity.d.ts +3 -0
- package/dist/utility/get-complexity.js +10 -0
- package/dist/utility/interfaces.d.ts +12 -0
- package/dist/utility/interfaces.js +8 -0
- package/dist/utility/lang.d.ts +2 -0
- package/dist/utility/lang.js +9 -0
- package/dist/utility/validate-complexity.d.ts +3 -0
- package/dist/utility/validate-complexity.js +33 -0
- package/dist/utility/validate-password-complexity.d.ts +14 -0
- package/dist/utility/validate-password-complexity.js +65 -0
- package/package.json +3 -2
- package/src/components/Password/index.tsx +5 -4
- package/src/components/PasswordComplexityFormField/index.tsx +34 -0
- package/src/forms/LoginForm/server.ts +3 -3
- package/src/forms/PasswordForm/index.tsx +86 -8
- package/src/forms/PasswordForm/server.ts +13 -7
- package/src/forms/PasswordResetForm/index.tsx +10 -4
- package/src/forms/PasswordResetForm/server.ts +11 -7
- package/src/mail/PasswordMail.tsx +16 -0
- package/src/rules/complexity.ts +14 -0
- package/src/utility/create-complexity-schema.ts +44 -0
- package/src/utility/get-complexity.ts +14 -0
- package/src/utility/interfaces.ts +11 -0
- package/src/utility/lang.ts +14 -0
- package/src/utility/validate-complexity.ts +53 -0
package/.turbo/turbo-dev.log
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
> @sqrzro/auth@4.0.0-alpha.8 dev /Users/richard/Sites/@sqrzro/sqrzro/packages/auth
|
|
3
3
|
> tsc --watch
|
|
4
4
|
|
|
5
|
-
[2J[3J[H[[
|
|
5
|
+
[2J[3J[H[[90m3:09:10 PM[0m] Starting compilation in watch mode...
|
|
6
6
|
|
|
7
|
-
[[
|
|
7
|
+
[[90m3:09:12 PM[0m] Found 0 errors. Watching for file changes.
|
|
8
8
|
|
|
9
|
-
[2J[3J[H[[
|
|
9
|
+
[2J[3J[H[[90m3:09:16 PM[0m] File change detected. Starting incremental compilation...
|
|
10
10
|
|
|
11
11
|
[96msrc/components/LogoutButton/server.ts[0m:[93m3[0m:[93m31[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module '@sqrzro/server/auth'. '/Users/richard/Sites/@sqrzro/sqrzro/packages/server/dist/auth/index.js' implicitly has an 'any' type.
|
|
12
12
|
Try `npm i --save-dev @types/sqrzro__server` if it exists or add a new declaration (.d.ts) file containing `declare module '@sqrzro/server/auth';`
|
|
@@ -50,6 +50,12 @@
|
|
|
50
50
|
[7m4[0m import { FormResponse, submitForm } from '@sqrzro/server/forms';
|
|
51
51
|
[7m [0m [91m ~~~~~~~~~~~~~~~~~~~~~~[0m
|
|
52
52
|
|
|
53
|
+
[96msrc/forms/PasswordForm/server.ts[0m:[93m5[0m:[93m26[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module '@sqrzro/server/mail'. '/Users/richard/Sites/@sqrzro/sqrzro/packages/server/dist/mail/index.js' implicitly has an 'any' type.
|
|
54
|
+
Try `npm i --save-dev @types/sqrzro__server` if it exists or add a new declaration (.d.ts) file containing `declare module '@sqrzro/server/mail';`
|
|
55
|
+
|
|
56
|
+
[7m5[0m import { sendMail } from '@sqrzro/server/mail';
|
|
57
|
+
[7m [0m [91m ~~~~~~~~~~~~~~~~~~~~~[0m
|
|
58
|
+
|
|
53
59
|
[96msrc/forms/PasswordResetForm/server.ts[0m:[93m3[0m:[93m41[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module '@sqrzro/server/auth'. '/Users/richard/Sites/@sqrzro/sqrzro/packages/server/dist/auth/index.js' implicitly has an 'any' type.
|
|
54
60
|
Try `npm i --save-dev @types/sqrzro__server` if it exists or add a new declaration (.d.ts) file containing `declare module '@sqrzro/server/auth';`
|
|
55
61
|
|
|
@@ -74,50 +80,28 @@
|
|
|
74
80
|
[7m2[0m import type { ProxyRedirect } from '@sqrzro/server/proxy';
|
|
75
81
|
[7m [0m [91m ~~~~~~~~~~~~~~~~~~~~~~[0m
|
|
76
82
|
|
|
77
|
-
[[
|
|
78
|
-
|
|
79
|
-
[2J[3J[H[[90m2:49:15 PM[0m] File change detected. Starting incremental compilation...
|
|
80
|
-
|
|
81
|
-
[[90m2:49:15 PM[0m] Found 0 errors. Watching for file changes.
|
|
82
|
-
|
|
83
|
-
[2J[3J[H[[90m2:49:47 PM[0m] File change detected. Starting incremental compilation...
|
|
84
|
-
|
|
85
|
-
[[90m2:49:47 PM[0m] Found 0 errors. Watching for file changes.
|
|
86
|
-
|
|
87
|
-
[2J[3J[H[[90m2:49:52 PM[0m] File change detected. Starting incremental compilation...
|
|
88
|
-
|
|
89
|
-
[[90m2:49:52 PM[0m] Found 0 errors. Watching for file changes.
|
|
90
|
-
|
|
91
|
-
[2J[3J[H[[90m2:50:44 PM[0m] File change detected. Starting incremental compilation...
|
|
92
|
-
|
|
93
|
-
[[90m2:50:44 PM[0m] Found 0 errors. Watching for file changes.
|
|
94
|
-
|
|
95
|
-
[2J[3J[H[[90m2:50:48 PM[0m] File change detected. Starting incremental compilation...
|
|
96
|
-
|
|
97
|
-
[[90m2:50:48 PM[0m] Found 0 errors. Watching for file changes.
|
|
98
|
-
|
|
99
|
-
[2J[3J[H[[90m2:59:21 PM[0m] File change detected. Starting incremental compilation...
|
|
100
|
-
|
|
101
|
-
[[90m2:59:21 PM[0m] Found 0 errors. Watching for file changes.
|
|
83
|
+
[96msrc/mail/PasswordMail.tsx[0m:[93m1[0m:[93m28[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module '@sqrzro/server/mail'. '/Users/richard/Sites/@sqrzro/sqrzro/packages/server/dist/mail/index.js' implicitly has an 'any' type.
|
|
84
|
+
Try `npm i --save-dev @types/sqrzro__server` if it exists or add a new declaration (.d.ts) file containing `declare module '@sqrzro/server/mail';`
|
|
102
85
|
|
|
103
|
-
[
|
|
86
|
+
[7m1[0m import { createMail } from '@sqrzro/server/mail';
|
|
87
|
+
[7m [0m [91m ~~~~~~~~~~~~~~~~~~~~~[0m
|
|
104
88
|
|
|
105
|
-
[[
|
|
89
|
+
[[90m3:09:16 PM[0m] Found 13 errors. Watching for file changes.
|
|
106
90
|
|
|
107
|
-
[2J[3J[H[[
|
|
91
|
+
[2J[3J[H[[90m3:09:20 PM[0m] File change detected. Starting incremental compilation...
|
|
108
92
|
|
|
109
|
-
[[
|
|
93
|
+
[[90m3:09:20 PM[0m] Found 0 errors. Watching for file changes.
|
|
110
94
|
|
|
111
|
-
[2J[3J[H[[
|
|
95
|
+
[2J[3J[H[[90m4:40:34 PM[0m] File change detected. Starting incremental compilation...
|
|
112
96
|
|
|
113
|
-
[[
|
|
97
|
+
[[90m4:40:34 PM[0m] Found 0 errors. Watching for file changes.
|
|
114
98
|
|
|
115
|
-
[2J[3J[H[[
|
|
99
|
+
[2J[3J[H[[90m4:40:39 PM[0m] File change detected. Starting incremental compilation...
|
|
116
100
|
|
|
117
|
-
[[
|
|
101
|
+
[[90m4:40:39 PM[0m] Found 0 errors. Watching for file changes.
|
|
118
102
|
|
|
119
|
-
[2J[3J[H[[
|
|
103
|
+
[2J[3J[H[[90m4:40:45 PM[0m] File change detected. Starting incremental compilation...
|
|
120
104
|
|
|
121
|
-
[[
|
|
105
|
+
[[90m4:40:45 PM[0m] Found 0 errors. Watching for file changes.
|
|
122
106
|
|
|
123
107
|
[41m[30m ELIFECYCLE [39m[49m [31mCommand failed.[39m
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { NextPageProps } from '@sqrzro/ui/utility';
|
|
1
2
|
import type { AuthClassNameProps } from '../../interfaces';
|
|
2
|
-
interface PasswordProps extends AuthClassNameProps {
|
|
3
|
-
searchParams?: Promise<Record<string, string>> | null;
|
|
3
|
+
interface PasswordProps extends AuthClassNameProps, NextPageProps {
|
|
4
4
|
}
|
|
5
5
|
declare function Password({ classNames, searchParams, }: Readonly<PasswordProps>): Promise<React.ReactElement>;
|
|
6
6
|
export default Password;
|
|
@@ -13,6 +13,6 @@ async function Password({ classNames, searchParams, }) {
|
|
|
13
13
|
return _jsx("div", { children: "Invalid or Expired Token" });
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
return _jsx(PasswordForm, { classNames: classNames });
|
|
16
|
+
return (_jsx(PasswordForm, { classNames: classNames, defaults: { email: awaitedSearchParams?.email } }));
|
|
17
17
|
}
|
|
18
18
|
export default Password;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PasswordFormFieldProps } from '@sqrzro/ui/forms';
|
|
2
|
+
import type { PasswordComplexityObject } from '../../utility/interfaces';
|
|
3
|
+
interface PasswordComplexityFormFieldProps extends PasswordFormFieldProps {
|
|
4
|
+
complexity?: PasswordComplexityObject;
|
|
5
|
+
onComplexity?: (isValid: boolean) => void;
|
|
6
|
+
}
|
|
7
|
+
declare function PasswordComplexityFormField({ complexity, onComplexity, value, ...props }: PasswordComplexityFormFieldProps): React.ReactElement;
|
|
8
|
+
export default PasswordComplexityFormField;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Fragment } from 'react';
|
|
4
|
+
import { PasswordComplexity, PasswordFormField } from '@sqrzro/ui/forms';
|
|
5
|
+
import getComplexity from '../../utility/get-complexity';
|
|
6
|
+
import validatePasswordComplexity from '../../utility/validate-complexity';
|
|
7
|
+
function PasswordComplexityFormField({ complexity, onComplexity, value, ...props }) {
|
|
8
|
+
return (_jsxs(Fragment, { children: [_jsx(PasswordFormField, { value: value, ...props }), _jsx(PasswordComplexity, { data: validatePasswordComplexity(getComplexity(complexity), value), onComplexity: onComplexity })] }));
|
|
9
|
+
}
|
|
10
|
+
export default PasswordComplexityFormField;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PasswordInputProps } from '@sqrzro/ui/forms';
|
|
2
|
+
interface PasswordComplexityInputProps extends PasswordInputProps {
|
|
3
|
+
}
|
|
4
|
+
declare function PasswordComplexityInput({ value, ...props }: PasswordComplexityInputProps): React.ReactElement;
|
|
5
|
+
export default PasswordComplexityInput;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { PasswordComplexity, PasswordInput } from '@sqrzro/ui/forms';
|
|
4
|
+
import getComplexity from '../../utility/get-complexity';
|
|
5
|
+
import validatePasswordComplexity from '../../utility/validate-complexity';
|
|
6
|
+
function PasswordComplexityInput({ value, ...props }) {
|
|
7
|
+
return (_jsxs("div", { children: [_jsx(PasswordInput, { value: value, ...props }), _jsx(PasswordComplexity, { data: validatePasswordComplexity(getComplexity(), value) })] }));
|
|
8
|
+
}
|
|
9
|
+
export default PasswordComplexityInput;
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -6,8 +6,8 @@ import { redirect } from 'next/navigation';
|
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
const schema = z
|
|
8
8
|
.object({
|
|
9
|
-
email: z.email(),
|
|
10
|
-
password: z.string(),
|
|
9
|
+
email: z.email().nonempty(),
|
|
10
|
+
password: z.string().nonempty(),
|
|
11
11
|
redirect: z.string().optional(),
|
|
12
12
|
})
|
|
13
13
|
.required({ email: true, password: true });
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { Default } from '@sqrzro/ui/forms';
|
|
1
2
|
import type { AuthClassNameProps } from '../../interfaces';
|
|
2
|
-
|
|
3
|
+
import type { PasswordFormFields } from './interfaces';
|
|
4
|
+
interface PasswordFormProps extends AuthClassNameProps {
|
|
5
|
+
defaults?: Default<PasswordFormFields>;
|
|
6
|
+
}
|
|
7
|
+
declare function PasswordForm({ classNames, defaults }: PasswordFormProps): React.ReactElement | null;
|
|
3
8
|
export default PasswordForm;
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Fragment } from 'react';
|
|
3
|
+
import { Fragment, useState } from 'react';
|
|
4
|
+
import { Link } from '@sqrzro/ui/components';
|
|
4
5
|
import { Form, FormSubmit, TextFormField, useForm } from '@sqrzro/ui/forms';
|
|
5
6
|
import submit from './server';
|
|
6
|
-
function PasswordForm({ classNames }) {
|
|
7
|
-
const
|
|
7
|
+
function PasswordForm({ classNames, defaults }) {
|
|
8
|
+
const [sentCount, setSentCount] = useState(0);
|
|
9
|
+
const { fieldProps, formData, formProps, setFormData, submitForm } = useForm({
|
|
10
|
+
defaults,
|
|
8
11
|
onSubmit: submit,
|
|
12
|
+
onSuccess: () => {
|
|
13
|
+
setSentCount(sentCount + 1);
|
|
14
|
+
},
|
|
9
15
|
});
|
|
10
|
-
|
|
16
|
+
function handleResend(event) {
|
|
17
|
+
event.preventDefault();
|
|
18
|
+
submitForm();
|
|
19
|
+
}
|
|
20
|
+
function handleReset(event) {
|
|
21
|
+
event.preventDefault();
|
|
22
|
+
setFormData('email', '');
|
|
23
|
+
setSentCount(0);
|
|
24
|
+
}
|
|
25
|
+
return (_jsxs(Fragment, { children: [_jsx("div", { style: { display: sentCount === 0 ? 'block' : 'none' }, children: _jsxs(Form, { ...formProps, children: [_jsx("h1", { className: classNames?.title, children: "Reset Your Password" }), _jsx("p", { className: "text-sm", children: "Enter the email address associated with your account and we'll send you a link to reset your password." }), _jsx(TextFormField, { ...fieldProps('email'), hasAssistiveError: true }), _jsx("div", { className: classNames?.actions, children: _jsx(FormSubmit, { isFullWidth: true, children: "Send Email" }) }), _jsx("footer", { className: classNames?.footer, children: _jsx(Link, { className: classNames?.link, href: "/auth/login", children: "Back" }) })] }) }), _jsx("div", { style: { display: sentCount === 0 ? 'none' : 'block' }, children: _jsxs("div", { className: classNames?.form, children: [_jsx("h1", { className: classNames?.title, children: "Check Your Email" }), sentCount === 1 ? (_jsxs(Fragment, { children: [_jsxs("p", { className: "text-sm", children: ["If ", _jsx("strong", { children: formData.email }), " matches an email we have on file, then we've sent you an email containing further instructions for resetting your password."] }), _jsxs("p", { className: "text-sm", children: ["If you haven't received an email in 5 minutes, check your spam,", ' ', _jsx(Link, { className: classNames?.link, onClick: handleResend, children: "resend" }), ", or", ' ', _jsx(Link, { className: classNames?.link, onClick: handleReset, children: "try a different email address" }), "."] })] })) : (_jsxs(Fragment, { children: [_jsxs("p", { className: "text-sm", children: ["We've resent password reset instructions to", ' ', _jsx("strong", { children: formData.email }), ", if it is an email we have on file."] }), _jsxs("p", { className: "text-sm", children: ["Please check again. If you still haven't received an email,", ' ', _jsx(Link, { className: classNames?.link, onClick: handleReset, children: "try a different email address" }), "."] })] }))] }) })] }));
|
|
11
26
|
}
|
|
12
27
|
export default PasswordForm;
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
'use server';
|
|
2
2
|
import { createReset } from '@sqrzro/server/auth';
|
|
3
3
|
import { submitForm } from '@sqrzro/server/forms';
|
|
4
|
+
import { sendMail } from '@sqrzro/server/mail';
|
|
4
5
|
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
email: z.email(),
|
|
8
|
-
})
|
|
9
|
-
.required({ email: true });
|
|
6
|
+
import PasswordMail from '../../mail/PasswordMail';
|
|
7
|
+
const schema = z.object({
|
|
8
|
+
email: z.email().nonempty(),
|
|
9
|
+
});
|
|
10
10
|
async function fn(data) {
|
|
11
11
|
const token = await createReset('PASSWORD', data.email);
|
|
12
|
-
|
|
12
|
+
if (!token) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
await sendMail(data.email, PasswordMail, { token });
|
|
13
16
|
}
|
|
14
17
|
async function submit(formData) {
|
|
15
18
|
return submitForm({
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Fragment } from 'react';
|
|
4
|
-
import { Form, FormSubmit,
|
|
3
|
+
import { Fragment, useState } from 'react';
|
|
4
|
+
import { Form, FormSubmit, useForm } from '@sqrzro/ui/forms';
|
|
5
|
+
import PasswordComplexityFormField from '../../components/PasswordComplexityFormField';
|
|
5
6
|
import submit from './server';
|
|
6
7
|
function PasswordResetForm({ classNames, token, }) {
|
|
8
|
+
const [isDisabled, setIsDisabled] = useState(true);
|
|
7
9
|
const { fieldProps, formProps } = useForm({
|
|
8
10
|
defaults: { token },
|
|
9
11
|
onSubmit: submit,
|
|
10
12
|
});
|
|
11
|
-
return (_jsx(Fragment, { children: _jsxs(Form, { ...formProps, children: [_jsx(
|
|
13
|
+
return (_jsx(Fragment, { children: _jsxs(Form, { ...formProps, children: [_jsx(PasswordComplexityFormField, { ...fieldProps('password'), onComplexity: (isValid) => setIsDisabled(!isValid) }), _jsx(FormSubmit, { isDisabled: isDisabled, children: "Reset Password" })] }) }));
|
|
12
14
|
}
|
|
13
15
|
export default PasswordResetForm;
|
|
@@ -3,12 +3,15 @@ import { updatePasswordWithToken } from '@sqrzro/server/auth';
|
|
|
3
3
|
import { submitForm } from '@sqrzro/server/forms';
|
|
4
4
|
import { redirect } from 'next/navigation';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
import getComplexityRule from '../../rules/complexity';
|
|
7
|
+
import getComplexity from '../../utility/get-complexity';
|
|
8
|
+
const schema = z.object({
|
|
9
|
+
password: getComplexityRule(getComplexity()).nonempty(),
|
|
10
|
+
token: z
|
|
11
|
+
.string()
|
|
12
|
+
.regex(/[A-Za-z0-9]{48}/u)
|
|
13
|
+
.nonempty(),
|
|
14
|
+
});
|
|
12
15
|
async function fn(data) {
|
|
13
16
|
await updatePasswordWithToken('PASSWORD', data.token, data.password);
|
|
14
17
|
return redirect('/auth/login');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createMail } from '@sqrzro/server/mail';
|
|
3
|
+
function PasswordMail({ token }) {
|
|
4
|
+
return (_jsxs("div", { children: ["PasswordMail", _jsx("a", { href: `http://localhost:8000/auth/password?token=${token}`, children: "Reset Password" })] }));
|
|
5
|
+
}
|
|
6
|
+
export default createMail('PASSWORD', PasswordMail);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import createComplexitySchema from '../utility/create-complexity-schema';
|
|
3
|
+
function getComplexityRule(complexity) {
|
|
4
|
+
const schema = createComplexitySchema(complexity);
|
|
5
|
+
return z.string().refine((value) => schema.safeParse(value).success, {
|
|
6
|
+
message: 'Password does not meet complexity requirements',
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export default getComplexityRule;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { PasswordComplexityKey } from './interfaces';
|
|
3
|
+
function createComplexitySchema(complexity) {
|
|
4
|
+
let result = z.string();
|
|
5
|
+
if (complexity.LENGTH) {
|
|
6
|
+
result = result.min(complexity.LENGTH, PasswordComplexityKey.LENGTH);
|
|
7
|
+
}
|
|
8
|
+
if (complexity.UPPERCASE) {
|
|
9
|
+
result = result.regex(new RegExp(`(?:.*[A-Z]){${complexity.UPPERCASE}}`), PasswordComplexityKey.UPPERCASE);
|
|
10
|
+
}
|
|
11
|
+
if (complexity.LOWERCASE) {
|
|
12
|
+
result = result.regex(new RegExp(`(?:.*[a-z]){${complexity.LOWERCASE}}`), PasswordComplexityKey.LOWERCASE);
|
|
13
|
+
}
|
|
14
|
+
if (complexity.NUMBER) {
|
|
15
|
+
result = result.regex(new RegExp(`(?:.*\\d){${complexity.NUMBER}}`), PasswordComplexityKey.NUMBER);
|
|
16
|
+
}
|
|
17
|
+
if (complexity.SPECIAL) {
|
|
18
|
+
result = result.regex(new RegExp(`(?:.*[@$!%*?&]){${complexity.SPECIAL}}`), PasswordComplexityKey.SPECIAL);
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
export default createComplexitySchema;
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare enum PasswordComplexityKey {
|
|
2
|
+
LENGTH = "LENGTH",
|
|
3
|
+
UPPERCASE = "UPPERCASE",
|
|
4
|
+
LOWERCASE = "LOWERCASE",
|
|
5
|
+
NUMBER = "NUMBER",
|
|
6
|
+
SPECIAL = "SPECIAL"
|
|
7
|
+
}
|
|
8
|
+
export type PasswordComplexityObject = Partial<Record<PasswordComplexityKey, number>>;
|
|
9
|
+
export type PasswordComplexityResult = {
|
|
10
|
+
status: boolean;
|
|
11
|
+
value: string;
|
|
12
|
+
}[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export var PasswordComplexityKey;
|
|
2
|
+
(function (PasswordComplexityKey) {
|
|
3
|
+
PasswordComplexityKey["LENGTH"] = "LENGTH";
|
|
4
|
+
PasswordComplexityKey["UPPERCASE"] = "UPPERCASE";
|
|
5
|
+
PasswordComplexityKey["LOWERCASE"] = "LOWERCASE";
|
|
6
|
+
PasswordComplexityKey["NUMBER"] = "NUMBER";
|
|
7
|
+
PasswordComplexityKey["SPECIAL"] = "SPECIAL";
|
|
8
|
+
})(PasswordComplexityKey || (PasswordComplexityKey = {}));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { formatPlural } from '@sqrzro/utility';
|
|
2
|
+
import { PasswordComplexityKey } from './interfaces';
|
|
3
|
+
export const complexities = {
|
|
4
|
+
[PasswordComplexityKey.LENGTH]: (value) => `At least ${formatPlural('character', value)} long`,
|
|
5
|
+
[PasswordComplexityKey.UPPERCASE]: (value) => `Contain at least ${formatPlural('uppercase letter', value)}`,
|
|
6
|
+
[PasswordComplexityKey.LOWERCASE]: (value) => `Contain at least ${formatPlural('lowercase letter', value)}`,
|
|
7
|
+
[PasswordComplexityKey.NUMBER]: (value) => `Contain at least ${formatPlural('number', value)}`,
|
|
8
|
+
[PasswordComplexityKey.SPECIAL]: (value) => `Contain at least ${formatPlural('special character', value)}`,
|
|
9
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getEntries, getKeys } from '@sqrzro/utility';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import createComplexitySchema from './create-complexity-schema';
|
|
4
|
+
import { complexities } from './lang';
|
|
5
|
+
function getDefaultComplexityResult(complexity) {
|
|
6
|
+
return getEntries(complexity).map(([key, value]) => ({
|
|
7
|
+
status: false,
|
|
8
|
+
value: complexities[key](value),
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
function transformResult(complexity, errors) {
|
|
12
|
+
// If there are errors that don't match the complexity keys (for example, the initial state), return the default result
|
|
13
|
+
if (errors.find((error) => !getKeys(complexity).includes(error))) {
|
|
14
|
+
return getDefaultComplexityResult(complexity);
|
|
15
|
+
}
|
|
16
|
+
return getEntries(complexity).map(([key, value]) => ({
|
|
17
|
+
status: !errors.includes(key),
|
|
18
|
+
value: complexities[key](value),
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
function validateComplexity(complexity, password) {
|
|
22
|
+
try {
|
|
23
|
+
createComplexitySchema(complexity).parse(password);
|
|
24
|
+
return transformResult(complexity, []);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error instanceof z.ZodError) {
|
|
28
|
+
return transformResult(complexity, error.issues.map((issue) => issue.message));
|
|
29
|
+
}
|
|
30
|
+
return getDefaultComplexityResult(complexity);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export default validateComplexity;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare enum PasswordComplexityKey {
|
|
2
|
+
LENGTH = "LENGTH",
|
|
3
|
+
UPPERCASE = "UPPERCASE",
|
|
4
|
+
LOWERCASE = "LOWERCASE",
|
|
5
|
+
NUMBER = "NUMBER",
|
|
6
|
+
SPECIAL = "SPECIAL"
|
|
7
|
+
}
|
|
8
|
+
export type PasswordComplexityObject = Partial<Record<PasswordComplexityKey, number>>;
|
|
9
|
+
type PasswordComplexityResult = {
|
|
10
|
+
status: boolean;
|
|
11
|
+
value: string;
|
|
12
|
+
}[];
|
|
13
|
+
declare function validatePasswordComplexity(complexity: PasswordComplexityObject, password?: string): PasswordComplexityResult;
|
|
14
|
+
export default validatePasswordComplexity;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { formatPlural, getEntries, getKeys } from '@sqrzro/utility';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export var PasswordComplexityKey;
|
|
4
|
+
(function (PasswordComplexityKey) {
|
|
5
|
+
PasswordComplexityKey["LENGTH"] = "LENGTH";
|
|
6
|
+
PasswordComplexityKey["UPPERCASE"] = "UPPERCASE";
|
|
7
|
+
PasswordComplexityKey["LOWERCASE"] = "LOWERCASE";
|
|
8
|
+
PasswordComplexityKey["NUMBER"] = "NUMBER";
|
|
9
|
+
PasswordComplexityKey["SPECIAL"] = "SPECIAL";
|
|
10
|
+
})(PasswordComplexityKey || (PasswordComplexityKey = {}));
|
|
11
|
+
const lang = {
|
|
12
|
+
[PasswordComplexityKey.LENGTH]: (value) => `At least ${formatPlural('character', value)} long`,
|
|
13
|
+
[PasswordComplexityKey.UPPERCASE]: (value) => `Contain at least ${formatPlural('uppercase letter', value)}`,
|
|
14
|
+
[PasswordComplexityKey.LOWERCASE]: (value) => `Contain at least ${formatPlural('lowercase letter', value)}`,
|
|
15
|
+
[PasswordComplexityKey.NUMBER]: (value) => `Contain at least ${formatPlural('number', value)}`,
|
|
16
|
+
[PasswordComplexityKey.SPECIAL]: (value) => `Contain at least ${formatPlural('special character', value)}`,
|
|
17
|
+
};
|
|
18
|
+
function createSchema(complexity) {
|
|
19
|
+
let result = z.string();
|
|
20
|
+
if (complexity.LENGTH) {
|
|
21
|
+
result = result.min(complexity.LENGTH, PasswordComplexityKey.LENGTH);
|
|
22
|
+
}
|
|
23
|
+
if (complexity.UPPERCASE) {
|
|
24
|
+
result = result.regex(new RegExp(`(?:.*[A-Z]){${complexity.UPPERCASE}}`), PasswordComplexityKey.UPPERCASE);
|
|
25
|
+
}
|
|
26
|
+
if (complexity.LOWERCASE) {
|
|
27
|
+
result = result.regex(new RegExp(`(?:.*[a-z]){${complexity.LOWERCASE}}`), PasswordComplexityKey.LOWERCASE);
|
|
28
|
+
}
|
|
29
|
+
if (complexity.NUMBER) {
|
|
30
|
+
result = result.regex(new RegExp(`(?:.*\\d){${complexity.NUMBER}}`), PasswordComplexityKey.NUMBER);
|
|
31
|
+
}
|
|
32
|
+
if (complexity.SPECIAL) {
|
|
33
|
+
result = result.regex(new RegExp(`(?:.*[@$!%*?&]){${complexity.SPECIAL}}`), PasswordComplexityKey.SPECIAL);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
function getDefaultComplexityResult(complexity) {
|
|
38
|
+
return getEntries(complexity).map(([key, value]) => ({
|
|
39
|
+
status: false,
|
|
40
|
+
value: lang[key](value),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
function transformResult(complexity, errors) {
|
|
44
|
+
// If there are errors that don't match the complexity keys (for example, the initial state), return the default result
|
|
45
|
+
if (errors.find((error) => !getKeys(complexity).includes(error))) {
|
|
46
|
+
return getDefaultComplexityResult(complexity);
|
|
47
|
+
}
|
|
48
|
+
return getEntries(complexity).map(([key, value]) => ({
|
|
49
|
+
status: !errors.includes(key),
|
|
50
|
+
value: lang[key](value),
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
function validatePasswordComplexity(complexity, password) {
|
|
54
|
+
try {
|
|
55
|
+
createSchema(complexity).parse(password);
|
|
56
|
+
return transformResult(complexity, []);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error instanceof z.ZodError) {
|
|
60
|
+
return transformResult(complexity, error.issues.map((issue) => issue.message));
|
|
61
|
+
}
|
|
62
|
+
return getDefaultComplexityResult(complexity);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export default validatePasswordComplexity;
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sqrzro/auth",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.0.0-alpha.
|
|
4
|
+
"version": "4.0.0-alpha.9",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"zod": "^4.3.6",
|
|
9
9
|
"@sqrzro/server": "^4.0.0-alpha.18",
|
|
10
|
-
"@sqrzro/
|
|
10
|
+
"@sqrzro/utility": "^4.0.0-alpha.3",
|
|
11
|
+
"@sqrzro/ui": "^4.0.0-alpha.14"
|
|
11
12
|
},
|
|
12
13
|
"devDependencies": {
|
|
13
14
|
"@types/react": "^19.2.7",
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { validateReset } from '@sqrzro/server/auth';
|
|
2
|
+
import { NextPageProps } from '@sqrzro/ui/utility';
|
|
2
3
|
|
|
3
4
|
import PasswordForm from '../../forms/PasswordForm';
|
|
4
5
|
import PasswordResetForm from '../../forms/PasswordResetForm';
|
|
5
6
|
import type { AuthClassNameProps } from '../../interfaces';
|
|
6
7
|
|
|
7
|
-
interface PasswordProps extends AuthClassNameProps {
|
|
8
|
-
searchParams?: Promise<Record<string, string>> | null;
|
|
9
|
-
}
|
|
8
|
+
interface PasswordProps extends AuthClassNameProps, NextPageProps {}
|
|
10
9
|
|
|
11
10
|
async function Password({
|
|
12
11
|
classNames,
|
|
@@ -24,7 +23,9 @@ async function Password({
|
|
|
24
23
|
}
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
return
|
|
26
|
+
return (
|
|
27
|
+
<PasswordForm classNames={classNames} defaults={{ email: awaitedSearchParams?.email }} />
|
|
28
|
+
);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export default Password;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Fragment } from 'react';
|
|
4
|
+
|
|
5
|
+
import { PasswordComplexity, PasswordFormField } from '@sqrzro/ui/forms';
|
|
6
|
+
import type { PasswordFormFieldProps } from '@sqrzro/ui/forms';
|
|
7
|
+
|
|
8
|
+
import getComplexity from '../../utility/get-complexity';
|
|
9
|
+
import validatePasswordComplexity from '../../utility/validate-complexity';
|
|
10
|
+
import type { PasswordComplexityObject } from '../../utility/interfaces';
|
|
11
|
+
|
|
12
|
+
interface PasswordComplexityFormFieldProps extends PasswordFormFieldProps {
|
|
13
|
+
complexity?: PasswordComplexityObject;
|
|
14
|
+
onComplexity?: (isValid: boolean) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function PasswordComplexityFormField({
|
|
18
|
+
complexity,
|
|
19
|
+
onComplexity,
|
|
20
|
+
value,
|
|
21
|
+
...props
|
|
22
|
+
}: PasswordComplexityFormFieldProps): React.ReactElement {
|
|
23
|
+
return (
|
|
24
|
+
<Fragment>
|
|
25
|
+
<PasswordFormField value={value} {...props} />
|
|
26
|
+
<PasswordComplexity
|
|
27
|
+
data={validatePasswordComplexity(getComplexity(complexity), value)}
|
|
28
|
+
onComplexity={onComplexity}
|
|
29
|
+
/>
|
|
30
|
+
</Fragment>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default PasswordComplexityFormField;
|
|
@@ -10,13 +10,13 @@ import type { LoginFormFields } from './interfaces';
|
|
|
10
10
|
|
|
11
11
|
const schema = z
|
|
12
12
|
.object({
|
|
13
|
-
email: z.email(),
|
|
14
|
-
password: z.string(),
|
|
13
|
+
email: z.email().nonempty(),
|
|
14
|
+
password: z.string().nonempty(),
|
|
15
15
|
redirect: z.string().optional(),
|
|
16
16
|
})
|
|
17
17
|
.required({ email: true, password: true });
|
|
18
18
|
|
|
19
|
-
async function fn(data:
|
|
19
|
+
async function fn(data: z.infer<typeof schema>): Promise<void> {
|
|
20
20
|
log('auth', 'LoginForm', `Login for email ${data.email}`);
|
|
21
21
|
|
|
22
22
|
const userID = await validateUser(data.email, data.password);
|
|
@@ -1,24 +1,102 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Fragment } from 'react';
|
|
3
|
+
import { Fragment, useState } from 'react';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Link } from '@sqrzro/ui/components';
|
|
6
|
+
import { Default, Form, FormSubmit, TextFormField, useForm } from '@sqrzro/ui/forms';
|
|
6
7
|
|
|
7
8
|
import type { AuthClassNameProps } from '../../interfaces';
|
|
8
9
|
|
|
9
10
|
import submit from './server';
|
|
11
|
+
import type { PasswordFormFields } from './interfaces';
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
interface PasswordFormProps extends AuthClassNameProps {
|
|
14
|
+
defaults?: Default<PasswordFormFields>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function PasswordForm({ classNames, defaults }: PasswordFormProps): React.ReactElement | null {
|
|
18
|
+
const [sentCount, setSentCount] = useState(0);
|
|
19
|
+
|
|
20
|
+
const { fieldProps, formData, formProps, setFormData, submitForm } = useForm({
|
|
21
|
+
defaults,
|
|
13
22
|
onSubmit: submit,
|
|
23
|
+
onSuccess: () => {
|
|
24
|
+
setSentCount(sentCount + 1);
|
|
25
|
+
},
|
|
14
26
|
});
|
|
15
27
|
|
|
28
|
+
function handleResend(event: React.MouseEvent<HTMLAnchorElement>): void {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
submitForm();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function handleReset(event: React.MouseEvent<HTMLAnchorElement>): void {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
|
|
36
|
+
setFormData('email', '');
|
|
37
|
+
setSentCount(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
16
40
|
return (
|
|
17
41
|
<Fragment>
|
|
18
|
-
<
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
42
|
+
<div style={{ display: sentCount === 0 ? 'block' : 'none' }}>
|
|
43
|
+
<Form {...formProps}>
|
|
44
|
+
<h1 className={classNames?.title}>Reset Your Password</h1>
|
|
45
|
+
<p className="text-sm">
|
|
46
|
+
Enter the email address associated with your account and we'll send you
|
|
47
|
+
a link to reset your password.
|
|
48
|
+
</p>
|
|
49
|
+
<TextFormField {...fieldProps('email')} hasAssistiveError />
|
|
50
|
+
<div className={classNames?.actions}>
|
|
51
|
+
<FormSubmit isFullWidth>Send Email</FormSubmit>
|
|
52
|
+
</div>
|
|
53
|
+
<footer className={classNames?.footer}>
|
|
54
|
+
<Link className={classNames?.link} href="/auth/login">
|
|
55
|
+
Back
|
|
56
|
+
</Link>
|
|
57
|
+
</footer>
|
|
58
|
+
</Form>
|
|
59
|
+
</div>
|
|
60
|
+
<div style={{ display: sentCount === 0 ? 'none' : 'block' }}>
|
|
61
|
+
<div className={classNames?.form}>
|
|
62
|
+
<h1 className={classNames?.title}>Check Your Email</h1>
|
|
63
|
+
{sentCount === 1 ? (
|
|
64
|
+
<Fragment>
|
|
65
|
+
<p className="text-sm">
|
|
66
|
+
If <strong>{formData.email}</strong> matches an email we have on
|
|
67
|
+
file, then we've sent you an email containing further
|
|
68
|
+
instructions for resetting your password.
|
|
69
|
+
</p>
|
|
70
|
+
<p className="text-sm">
|
|
71
|
+
If you haven't received an email in 5 minutes, check your spam,{' '}
|
|
72
|
+
<Link className={classNames?.link} onClick={handleResend}>
|
|
73
|
+
resend
|
|
74
|
+
</Link>
|
|
75
|
+
, or{' '}
|
|
76
|
+
<Link className={classNames?.link} onClick={handleReset}>
|
|
77
|
+
try a different email address
|
|
78
|
+
</Link>
|
|
79
|
+
.
|
|
80
|
+
</p>
|
|
81
|
+
</Fragment>
|
|
82
|
+
) : (
|
|
83
|
+
<Fragment>
|
|
84
|
+
<p className="text-sm">
|
|
85
|
+
We've resent password reset instructions to{' '}
|
|
86
|
+
<strong>{formData.email}</strong>, if it is an email we have on
|
|
87
|
+
file.
|
|
88
|
+
</p>
|
|
89
|
+
<p className="text-sm">
|
|
90
|
+
Please check again. If you still haven't received an email,{' '}
|
|
91
|
+
<Link className={classNames?.link} onClick={handleReset}>
|
|
92
|
+
try a different email address
|
|
93
|
+
</Link>
|
|
94
|
+
.
|
|
95
|
+
</p>
|
|
96
|
+
</Fragment>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
22
100
|
</Fragment>
|
|
23
101
|
);
|
|
24
102
|
}
|
|
@@ -2,19 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
import { createReset } from '@sqrzro/server/auth';
|
|
4
4
|
import { FormResponse, submitForm } from '@sqrzro/server/forms';
|
|
5
|
+
import { sendMail } from '@sqrzro/server/mail';
|
|
5
6
|
import { z } from 'zod';
|
|
6
7
|
|
|
8
|
+
import PasswordMail from '../../mail/PasswordMail';
|
|
9
|
+
|
|
7
10
|
import type { PasswordFormFields } from './interfaces';
|
|
8
11
|
|
|
9
|
-
const schema = z
|
|
10
|
-
.
|
|
11
|
-
|
|
12
|
-
})
|
|
13
|
-
.required({ email: true });
|
|
12
|
+
const schema = z.object({
|
|
13
|
+
email: z.email().nonempty(),
|
|
14
|
+
});
|
|
14
15
|
|
|
15
|
-
async function fn(data:
|
|
16
|
+
async function fn(data: z.infer<typeof schema>): Promise<void> {
|
|
16
17
|
const token = await createReset('PASSWORD', data.email);
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
if (!token) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await sendMail(data.email, PasswordMail, { token });
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
async function submit(formData: PasswordFormFields): FormResponse<void> {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Fragment } from 'react';
|
|
3
|
+
import { Fragment, useState } from 'react';
|
|
4
4
|
|
|
5
|
-
import { Form, FormSubmit,
|
|
5
|
+
import { Form, FormSubmit, useForm } from '@sqrzro/ui/forms';
|
|
6
6
|
|
|
7
|
+
import PasswordComplexityFormField from '../../components/PasswordComplexityFormField';
|
|
7
8
|
import type { AuthClassNameProps } from '../../interfaces';
|
|
8
9
|
|
|
9
10
|
import submit from './server';
|
|
@@ -16,6 +17,8 @@ function PasswordResetForm({
|
|
|
16
17
|
classNames,
|
|
17
18
|
token,
|
|
18
19
|
}: Readonly<PasswordResetFormProps>): React.ReactElement | null {
|
|
20
|
+
const [isDisabled, setIsDisabled] = useState(true);
|
|
21
|
+
|
|
19
22
|
const { fieldProps, formProps } = useForm({
|
|
20
23
|
defaults: { token },
|
|
21
24
|
onSubmit: submit,
|
|
@@ -24,8 +27,11 @@ function PasswordResetForm({
|
|
|
24
27
|
return (
|
|
25
28
|
<Fragment>
|
|
26
29
|
<Form {...formProps}>
|
|
27
|
-
<
|
|
28
|
-
|
|
30
|
+
<PasswordComplexityFormField
|
|
31
|
+
{...fieldProps('password')}
|
|
32
|
+
onComplexity={(isValid) => setIsDisabled(!isValid)}
|
|
33
|
+
/>
|
|
34
|
+
<FormSubmit isDisabled={isDisabled}>Reset Password</FormSubmit>
|
|
29
35
|
</Form>
|
|
30
36
|
</Fragment>
|
|
31
37
|
);
|
|
@@ -5,16 +5,20 @@ import { FormResponse, submitForm } from '@sqrzro/server/forms';
|
|
|
5
5
|
import { redirect } from 'next/navigation';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
|
|
8
|
+
import getComplexityRule from '../../rules/complexity';
|
|
9
|
+
import getComplexity from '../../utility/get-complexity';
|
|
10
|
+
|
|
8
11
|
import type { PasswordResetFormFields } from './interfaces';
|
|
9
12
|
|
|
10
|
-
const schema = z
|
|
11
|
-
.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const schema = z.object({
|
|
14
|
+
password: getComplexityRule(getComplexity()).nonempty(),
|
|
15
|
+
token: z
|
|
16
|
+
.string()
|
|
17
|
+
.regex(/[A-Za-z0-9]{48}/u)
|
|
18
|
+
.nonempty(),
|
|
19
|
+
});
|
|
16
20
|
|
|
17
|
-
async function fn(data:
|
|
21
|
+
async function fn(data: z.infer<typeof schema>): Promise<void> {
|
|
18
22
|
await updatePasswordWithToken('PASSWORD', data.token, data.password);
|
|
19
23
|
return redirect('/auth/login');
|
|
20
24
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createMail } from '@sqrzro/server/mail';
|
|
2
|
+
|
|
3
|
+
interface PasswordMailProps {
|
|
4
|
+
readonly token: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function PasswordMail({ token }: PasswordMailProps): React.ReactElement {
|
|
8
|
+
return (
|
|
9
|
+
<div>
|
|
10
|
+
PasswordMail
|
|
11
|
+
<a href={`http://localhost:8000/auth/password?token=${token}`}>Reset Password</a>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default createMail('PASSWORD', PasswordMail);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import createComplexitySchema from '../utility/create-complexity-schema';
|
|
4
|
+
import type { PasswordComplexityObject } from '../utility/interfaces';
|
|
5
|
+
|
|
6
|
+
function getComplexityRule(complexity: PasswordComplexityObject) {
|
|
7
|
+
const schema = createComplexitySchema(complexity);
|
|
8
|
+
|
|
9
|
+
return z.string().refine((value) => schema.safeParse(value).success, {
|
|
10
|
+
message: 'Password does not meet complexity requirements',
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default getComplexityRule;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { PasswordComplexityKey } from './interfaces';
|
|
4
|
+
import type { PasswordComplexityObject } from './interfaces';
|
|
5
|
+
|
|
6
|
+
function createComplexitySchema(complexity: PasswordComplexityObject): z.ZodString {
|
|
7
|
+
let result = z.string();
|
|
8
|
+
|
|
9
|
+
if (complexity.LENGTH) {
|
|
10
|
+
result = result.min(complexity.LENGTH, PasswordComplexityKey.LENGTH);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (complexity.UPPERCASE) {
|
|
14
|
+
result = result.regex(
|
|
15
|
+
new RegExp(`(?:.*[A-Z]){${complexity.UPPERCASE}}`),
|
|
16
|
+
PasswordComplexityKey.UPPERCASE
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (complexity.LOWERCASE) {
|
|
21
|
+
result = result.regex(
|
|
22
|
+
new RegExp(`(?:.*[a-z]){${complexity.LOWERCASE}}`),
|
|
23
|
+
PasswordComplexityKey.LOWERCASE
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (complexity.NUMBER) {
|
|
28
|
+
result = result.regex(
|
|
29
|
+
new RegExp(`(?:.*\\d){${complexity.NUMBER}}`),
|
|
30
|
+
PasswordComplexityKey.NUMBER
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (complexity.SPECIAL) {
|
|
35
|
+
result = result.regex(
|
|
36
|
+
new RegExp(`(?:.*[@$!%*?&]){${complexity.SPECIAL}}`),
|
|
37
|
+
PasswordComplexityKey.SPECIAL
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default createComplexitySchema;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PasswordComplexityObject } from './interfaces';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_COMPLEXITY: PasswordComplexityObject = {
|
|
4
|
+
LENGTH: 8,
|
|
5
|
+
LOWERCASE: 1,
|
|
6
|
+
UPPERCASE: 1,
|
|
7
|
+
NUMBER: 1,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function getComplexity(complexity?: PasswordComplexityObject): PasswordComplexityObject {
|
|
11
|
+
return complexity ?? DEFAULT_COMPLEXITY;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default getComplexity;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export enum PasswordComplexityKey {
|
|
2
|
+
LENGTH = 'LENGTH',
|
|
3
|
+
UPPERCASE = 'UPPERCASE',
|
|
4
|
+
LOWERCASE = 'LOWERCASE',
|
|
5
|
+
NUMBER = 'NUMBER',
|
|
6
|
+
SPECIAL = 'SPECIAL',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type PasswordComplexityObject = Partial<Record<PasswordComplexityKey, number>>;
|
|
10
|
+
|
|
11
|
+
export type PasswordComplexityResult = { status: boolean; value: string }[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { formatPlural } from '@sqrzro/utility';
|
|
2
|
+
|
|
3
|
+
import { PasswordComplexityKey } from './interfaces';
|
|
4
|
+
|
|
5
|
+
export const complexities: Record<PasswordComplexityKey, (value?: number) => string> = {
|
|
6
|
+
[PasswordComplexityKey.LENGTH]: (value) => `At least ${formatPlural('character', value)} long`,
|
|
7
|
+
[PasswordComplexityKey.UPPERCASE]: (value) =>
|
|
8
|
+
`Contain at least ${formatPlural('uppercase letter', value)}`,
|
|
9
|
+
[PasswordComplexityKey.LOWERCASE]: (value) =>
|
|
10
|
+
`Contain at least ${formatPlural('lowercase letter', value)}`,
|
|
11
|
+
[PasswordComplexityKey.NUMBER]: (value) => `Contain at least ${formatPlural('number', value)}`,
|
|
12
|
+
[PasswordComplexityKey.SPECIAL]: (value) =>
|
|
13
|
+
`Contain at least ${formatPlural('special character', value)}`,
|
|
14
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getEntries, getKeys } from '@sqrzro/utility';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import createComplexitySchema from './create-complexity-schema';
|
|
5
|
+
import { complexities } from './lang';
|
|
6
|
+
|
|
7
|
+
import { PasswordComplexityKey } from './interfaces';
|
|
8
|
+
import type { PasswordComplexityObject, PasswordComplexityResult } from './interfaces';
|
|
9
|
+
|
|
10
|
+
function getDefaultComplexityResult(
|
|
11
|
+
complexity: PasswordComplexityObject
|
|
12
|
+
): PasswordComplexityResult {
|
|
13
|
+
return getEntries(complexity).map(([key, value]) => ({
|
|
14
|
+
status: false,
|
|
15
|
+
value: complexities[key](value),
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function transformResult(
|
|
20
|
+
complexity: PasswordComplexityObject,
|
|
21
|
+
errors: PasswordComplexityKey[]
|
|
22
|
+
): PasswordComplexityResult {
|
|
23
|
+
// If there are errors that don't match the complexity keys (for example, the initial state), return the default result
|
|
24
|
+
|
|
25
|
+
if (errors.find((error) => !getKeys(complexity).includes(error))) {
|
|
26
|
+
return getDefaultComplexityResult(complexity);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return getEntries(complexity).map(([key, value]) => ({
|
|
30
|
+
status: !errors.includes(key),
|
|
31
|
+
value: complexities[key](value),
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function validateComplexity(
|
|
36
|
+
complexity: PasswordComplexityObject,
|
|
37
|
+
password?: string
|
|
38
|
+
): PasswordComplexityResult {
|
|
39
|
+
try {
|
|
40
|
+
createComplexitySchema(complexity).parse(password);
|
|
41
|
+
return transformResult(complexity, []);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error instanceof z.ZodError) {
|
|
44
|
+
return transformResult(
|
|
45
|
+
complexity,
|
|
46
|
+
error.issues.map((issue) => issue.message as PasswordComplexityKey)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return getDefaultComplexityResult(complexity);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default validateComplexity;
|