@sqrzro/auth 4.0.0-alpha.1 → 4.0.0-alpha.10
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-build.log +1 -1
- package/.turbo/turbo-dev.log +580 -72
- package/dist/components/Auth/index.d.ts +5 -4
- package/dist/components/Auth/index.js +6 -6
- package/dist/components/Password/index.d.ts +4 -3
- package/dist/components/Password/index.js +4 -4
- 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/index.d.ts +2 -1
- package/dist/forms/LoginForm/index.js +2 -2
- package/dist/forms/LoginForm/server.js +6 -2
- package/dist/forms/PasswordForm/index.d.ts +7 -1
- package/dist/forms/PasswordForm/index.js +19 -4
- package/dist/forms/PasswordForm/server.js +9 -6
- package/dist/forms/PasswordResetForm/index.d.ts +3 -2
- package/dist/forms/PasswordResetForm/index.js +6 -4
- package/dist/forms/PasswordResetForm/server.js +10 -7
- package/dist/get-auth-proxy.d.ts +6 -0
- package/dist/get-auth-proxy.js +17 -0
- package/dist/handle-auth-proxy.d.ts +5 -0
- package/dist/handle-auth-proxy.js +14 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -1
- package/dist/interfaces.d.ts +6 -0
- 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 +5 -4
- package/src/components/Auth/index.tsx +18 -13
- package/src/components/Password/index.tsx +12 -7
- package/src/components/PasswordComplexityFormField/index.tsx +34 -0
- package/src/forms/LoginForm/index.tsx +10 -4
- package/src/forms/LoginForm/server.ts +11 -3
- package/src/forms/PasswordForm/index.tsx +88 -8
- package/src/forms/PasswordForm/server.ts +13 -7
- package/src/forms/PasswordResetForm/index.tsx +17 -6
- package/src/forms/PasswordResetForm/server.ts +12 -8
- package/src/get-auth-proxy.ts +34 -0
- package/src/index.ts +5 -1
- package/src/interfaces.ts +9 -1
- 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/src/handle-proxy.ts +0 -23
|
@@ -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.10",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"@sqrzro/ui": "^4.0.0-alpha.
|
|
8
|
+
"zod": "^4.3.6",
|
|
9
|
+
"@sqrzro/server": "^4.0.0-alpha.20",
|
|
10
|
+
"@sqrzro/ui": "^4.0.0-alpha.20",
|
|
11
|
+
"@sqrzro/utility": "^4.0.0-alpha.7"
|
|
11
12
|
},
|
|
12
13
|
"devDependencies": {
|
|
13
14
|
"@types/react": "^19.2.7",
|
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
import { Suspense } from 'react';
|
|
2
2
|
|
|
3
|
+
import type { NextPageProps } from '@sqrzro/ui/utility';
|
|
3
4
|
import { notFound } from 'next/navigation';
|
|
4
5
|
|
|
5
6
|
import LoginForm from '../../forms/LoginForm';
|
|
7
|
+
import type { AuthClassNameProps } from '../../interfaces';
|
|
8
|
+
|
|
6
9
|
import Password from '../Password';
|
|
7
10
|
|
|
8
|
-
interface AuthProps {
|
|
9
|
-
|
|
10
|
-
searchParams: Promise<Record<string, string>>;
|
|
11
|
+
export interface AuthProps extends AuthClassNameProps, NextPageProps {
|
|
12
|
+
logo?: React.ReactElement;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
async function AuthComponent({
|
|
16
|
+
classNames,
|
|
14
17
|
params,
|
|
15
18
|
searchParams,
|
|
16
|
-
}:
|
|
19
|
+
}: AuthProps): Promise<React.ReactElement> {
|
|
17
20
|
const awaitedParams = await params;
|
|
18
21
|
|
|
19
|
-
if (!Array.isArray(awaitedParams.auth) || awaitedParams.auth.length !== 1) {
|
|
22
|
+
if (!awaitedParams || !Array.isArray(awaitedParams.auth) || awaitedParams.auth.length !== 1) {
|
|
20
23
|
return notFound();
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
if (awaitedParams.auth[0] === 'login') {
|
|
24
|
-
return <LoginForm />;
|
|
27
|
+
return <LoginForm classNames={classNames} />;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
if (awaitedParams.auth[0] === 'mfa') {
|
|
@@ -29,19 +32,21 @@ async function AuthComponent({
|
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
if (awaitedParams.auth[0] === 'password') {
|
|
32
|
-
return <Password searchParams={searchParams} />;
|
|
35
|
+
return <Password classNames={classNames} searchParams={searchParams} />;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
return notFound();
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
function Auth({
|
|
41
|
+
function Auth({ classNames, logo, ...props }: AuthProps): React.ReactElement {
|
|
39
42
|
return (
|
|
40
|
-
<div>
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
|
|
43
|
+
<div className={classNames?.root}>
|
|
44
|
+
<div className={classNames?.logo}>{logo}</div>
|
|
45
|
+
<div className={classNames?.panel}>
|
|
46
|
+
<Suspense>
|
|
47
|
+
<AuthComponent classNames={classNames} {...props} />
|
|
48
|
+
</Suspense>
|
|
49
|
+
</div>
|
|
45
50
|
</div>
|
|
46
51
|
);
|
|
47
52
|
}
|
|
@@ -1,26 +1,31 @@
|
|
|
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';
|
|
6
|
+
import type { AuthClassNameProps } from '../../interfaces';
|
|
5
7
|
|
|
6
|
-
interface PasswordProps {
|
|
7
|
-
searchParams: Promise<Record<string, string>>;
|
|
8
|
-
}
|
|
8
|
+
interface PasswordProps extends AuthClassNameProps, NextPageProps {}
|
|
9
9
|
|
|
10
|
-
async function Password({
|
|
10
|
+
async function Password({
|
|
11
|
+
classNames,
|
|
12
|
+
searchParams,
|
|
13
|
+
}: Readonly<PasswordProps>): Promise<React.ReactElement> {
|
|
11
14
|
const awaitedSearchParams = await searchParams;
|
|
12
15
|
|
|
13
|
-
if (awaitedSearchParams
|
|
16
|
+
if (awaitedSearchParams?.token) {
|
|
14
17
|
const validated = await validateReset('PASSWORD', awaitedSearchParams.token);
|
|
15
18
|
|
|
16
19
|
if (validated) {
|
|
17
|
-
return <PasswordResetForm token={awaitedSearchParams.token} />;
|
|
20
|
+
return <PasswordResetForm token={awaitedSearchParams.token} classNames={classNames} />;
|
|
18
21
|
} else {
|
|
19
22
|
return <div>Invalid or Expired Token</div>;
|
|
20
23
|
}
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
return
|
|
26
|
+
return (
|
|
27
|
+
<PasswordForm classNames={classNames} defaults={{ email: awaitedSearchParams?.email }} />
|
|
28
|
+
);
|
|
24
29
|
}
|
|
25
30
|
|
|
26
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;
|
|
@@ -5,21 +5,27 @@ import { Fragment } from 'react';
|
|
|
5
5
|
import { Link } from '@sqrzro/ui/components';
|
|
6
6
|
import { Form, FormSubmit, PasswordFormField, TextFormField, useForm } from '@sqrzro/ui/forms';
|
|
7
7
|
|
|
8
|
+
import type { AuthClassNameProps } from '../../interfaces';
|
|
9
|
+
|
|
8
10
|
import submit from './server';
|
|
9
11
|
|
|
10
|
-
function LoginForm(): React.ReactElement | null {
|
|
12
|
+
function LoginForm({ classNames }: AuthClassNameProps): React.ReactElement | null {
|
|
11
13
|
const { fieldProps, formData, formProps } = useForm({
|
|
12
14
|
onSubmit: submit,
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
return (
|
|
16
18
|
<Fragment>
|
|
17
|
-
<
|
|
19
|
+
<h1 className={classNames?.title}>Sign in to continue</h1>
|
|
20
|
+
<Form classNames={{ root: classNames?.form }} {...formProps}>
|
|
18
21
|
<TextFormField {...fieldProps('email')} hasAssistiveError />
|
|
19
22
|
<PasswordFormField {...fieldProps('password')} hasAssistiveError />
|
|
20
|
-
<
|
|
21
|
-
|
|
23
|
+
<div className={classNames?.actions}>
|
|
24
|
+
<FormSubmit isFullWidth>Sign In</FormSubmit>
|
|
25
|
+
</div>
|
|
26
|
+
<footer className={classNames?.footer}>
|
|
22
27
|
<Link
|
|
28
|
+
className={classNames?.link}
|
|
23
29
|
href={`/auth/password${formData.email ? `?email=${formData.email}` : ''}`}
|
|
24
30
|
>
|
|
25
31
|
Forgot Password?
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { createSession, validateUser } from '@sqrzro/server/auth';
|
|
4
4
|
import { FormResponse, submitForm } from '@sqrzro/server/forms';
|
|
5
|
+
import { log } from '@sqrzro/server/utility';
|
|
5
6
|
import { redirect } from 'next/navigation';
|
|
6
7
|
import { z } from 'zod';
|
|
7
8
|
|
|
@@ -9,16 +10,23 @@ import type { LoginFormFields } from './interfaces';
|
|
|
9
10
|
|
|
10
11
|
const schema = z
|
|
11
12
|
.object({
|
|
12
|
-
email: z.email(),
|
|
13
|
-
password: z.string(),
|
|
13
|
+
email: z.email().nonempty(),
|
|
14
|
+
password: z.string().nonempty(),
|
|
14
15
|
redirect: z.string().optional(),
|
|
15
16
|
})
|
|
16
17
|
.required({ email: true, password: true });
|
|
17
18
|
|
|
18
|
-
async function fn(data:
|
|
19
|
+
async function fn(data: z.infer<typeof schema>): Promise<void> {
|
|
20
|
+
log('auth', 'LoginForm', `Login for email ${data.email}`);
|
|
21
|
+
|
|
19
22
|
const userID = await validateUser(data.email, data.password);
|
|
23
|
+
|
|
24
|
+
log('auth', 'LoginForm', `Validated user ${userID} for email ${data.email}`);
|
|
25
|
+
|
|
20
26
|
await createSession(userID);
|
|
21
27
|
|
|
28
|
+
log('auth', 'LoginForm', `Session created for user ${userID}`);
|
|
29
|
+
|
|
22
30
|
redirect('/');
|
|
23
31
|
}
|
|
24
32
|
|
|
@@ -1,22 +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';
|
|
7
|
+
|
|
8
|
+
import type { AuthClassNameProps } from '../../interfaces';
|
|
6
9
|
|
|
7
10
|
import submit from './server';
|
|
11
|
+
import type { PasswordFormFields } from './interfaces';
|
|
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);
|
|
8
19
|
|
|
9
|
-
|
|
10
|
-
|
|
20
|
+
const { fieldProps, formData, formProps, setFormData, submitForm } = useForm({
|
|
21
|
+
defaults,
|
|
11
22
|
onSubmit: submit,
|
|
23
|
+
onSuccess: () => {
|
|
24
|
+
setSentCount(sentCount + 1);
|
|
25
|
+
},
|
|
12
26
|
});
|
|
13
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
|
+
|
|
14
40
|
return (
|
|
15
41
|
<Fragment>
|
|
16
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
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>
|
|
20
100
|
</Fragment>
|
|
21
101
|
);
|
|
22
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,16 +1,24 @@
|
|
|
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
|
+
|
|
7
|
+
import PasswordComplexityFormField from '../../components/PasswordComplexityFormField';
|
|
8
|
+
import type { AuthClassNameProps } from '../../interfaces';
|
|
6
9
|
|
|
7
10
|
import submit from './server';
|
|
8
11
|
|
|
9
|
-
interface PasswordResetFormProps {
|
|
12
|
+
interface PasswordResetFormProps extends AuthClassNameProps {
|
|
10
13
|
token: string;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
function PasswordResetForm({
|
|
16
|
+
function PasswordResetForm({
|
|
17
|
+
classNames,
|
|
18
|
+
token,
|
|
19
|
+
}: Readonly<PasswordResetFormProps>): React.ReactElement | null {
|
|
20
|
+
const [isDisabled, setIsDisabled] = useState(true);
|
|
21
|
+
|
|
14
22
|
const { fieldProps, formProps } = useForm({
|
|
15
23
|
defaults: { token },
|
|
16
24
|
onSubmit: submit,
|
|
@@ -19,8 +27,11 @@ function PasswordResetForm({ token }: Readonly<PasswordResetFormProps>): React.R
|
|
|
19
27
|
return (
|
|
20
28
|
<Fragment>
|
|
21
29
|
<Form {...formProps}>
|
|
22
|
-
<
|
|
23
|
-
|
|
30
|
+
<PasswordComplexityFormField
|
|
31
|
+
{...fieldProps('password')}
|
|
32
|
+
onComplexity={(isValid) => setIsDisabled(!isValid)}
|
|
33
|
+
/>
|
|
34
|
+
<FormSubmit isDisabled={isDisabled}>Reset Password</FormSubmit>
|
|
24
35
|
</Form>
|
|
25
36
|
</Fragment>
|
|
26
37
|
);
|
|
@@ -5,17 +5,21 @@ 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:
|
|
18
|
-
await updatePasswordWithToken(data.token, data.password);
|
|
21
|
+
async function fn(data: z.infer<typeof schema>): Promise<void> {
|
|
22
|
+
await updatePasswordWithToken('PASSWORD', data.token, data.password);
|
|
19
23
|
return redirect('/auth/login');
|
|
20
24
|
}
|
|
21
25
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AuthSession } from '@sqrzro/server/auth';
|
|
2
|
+
import type { ProxyRedirect } from '@sqrzro/server/proxy';
|
|
3
|
+
import type { NextRequest } from 'next/server';
|
|
4
|
+
|
|
5
|
+
import type { AuthConfigObject } from './interfaces';
|
|
6
|
+
|
|
7
|
+
async function handleAuthProxy(
|
|
8
|
+
session: AuthSession | null,
|
|
9
|
+
request: NextRequest,
|
|
10
|
+
config?: AuthConfigObject
|
|
11
|
+
): Promise<ProxyRedirect | null> {
|
|
12
|
+
const pathname = request.nextUrl.pathname;
|
|
13
|
+
|
|
14
|
+
if (session) {
|
|
15
|
+
if (pathname.startsWith('/auth')) {
|
|
16
|
+
return { redirect: '/' };
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (pathname.startsWith('/auth') || config?.allow?.includes(pathname)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { redirect: '/auth/login' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getAuthProxy(
|
|
29
|
+
config?: AuthConfigObject
|
|
30
|
+
): (session: AuthSession | null, request: NextRequest) => Promise<ProxyRedirect | null> {
|
|
31
|
+
return (session, request) => handleAuthProxy(session, request, config);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default getAuthProxy;
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
export type { AuthClassNames, AuthConfigObject } from './interfaces';
|
|
2
|
+
|
|
3
|
+
export type { AuthProps } from './components/Auth';
|
|
1
4
|
export { default as Auth } from './components/Auth';
|
|
5
|
+
|
|
2
6
|
export { default as LogoutButton } from './components/LogoutButton';
|
|
3
7
|
|
|
4
|
-
export { default as
|
|
8
|
+
export { default as getAuthProxy } from './get-auth-proxy';
|
package/src/interfaces.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export interface AuthConfigObject {
|
|
2
|
+
allow?: string[];
|
|
3
|
+
}
|
|
4
|
+
|
|
1
5
|
export interface AuthClassNames {
|
|
2
6
|
actions: string;
|
|
3
7
|
footer: string;
|
|
@@ -7,4 +11,8 @@ export interface AuthClassNames {
|
|
|
7
11
|
root: string;
|
|
8
12
|
panel: string;
|
|
9
13
|
title: string;
|
|
10
|
-
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AuthClassNameProps {
|
|
17
|
+
classNames?: Partial<AuthClassNames>;
|
|
18
|
+
}
|
|
@@ -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;
|