@sqrzro/auth 2.0.0-bz.0

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 (43) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/LICENSE +5 -0
  3. package/dist/components/Auth/index.d.ts +33 -0
  4. package/dist/components/Auth/index.js +46 -0
  5. package/dist/components/LoginForm/index.d.ts +10 -0
  6. package/dist/components/LoginForm/index.js +13 -0
  7. package/dist/components/LogoutButton/index.d.ts +7 -0
  8. package/dist/components/LogoutButton/index.js +13 -0
  9. package/dist/components/MFAForm/index.d.ts +8 -0
  10. package/dist/components/MFAForm/index.js +29 -0
  11. package/dist/components/MFAPage/index.d.ts +7 -0
  12. package/dist/components/MFAPage/index.js +21 -0
  13. package/dist/components/MFASetup/index.d.ts +7 -0
  14. package/dist/components/MFASetup/index.js +20 -0
  15. package/dist/components/MFASetupForm/index.d.ts +8 -0
  16. package/dist/components/MFASetupForm/index.js +20 -0
  17. package/dist/components/PasswordForm/index.d.ts +8 -0
  18. package/dist/components/PasswordForm/index.js +28 -0
  19. package/dist/components/PasswordPage/index.d.ts +11 -0
  20. package/dist/components/PasswordPage/index.js +10 -0
  21. package/dist/components/PasswordResetForm/index.d.ts +8 -0
  22. package/dist/components/PasswordResetForm/index.js +20 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.js +2 -0
  25. package/dist/server.d.ts +20 -0
  26. package/dist/server.js +46 -0
  27. package/next-env.d.ts +5 -0
  28. package/package.json +22 -0
  29. package/prettier.config.cjs +1 -0
  30. package/src/components/Auth/index.tsx +95 -0
  31. package/src/components/LoginForm/index.tsx +44 -0
  32. package/src/components/LogoutButton/index.tsx +30 -0
  33. package/src/components/MFAForm/index.tsx +59 -0
  34. package/src/components/MFAPage/index.js +78 -0
  35. package/src/components/MFAPage/index.tsx +42 -0
  36. package/src/components/MFASetup/index.tsx +39 -0
  37. package/src/components/MFASetupForm/index.tsx +62 -0
  38. package/src/components/PasswordForm/index.tsx +107 -0
  39. package/src/components/PasswordPage/index.tsx +20 -0
  40. package/src/components/PasswordResetForm/index.tsx +50 -0
  41. package/src/index.ts +3 -0
  42. package/src/server.ts +87 -0
  43. package/tsconfig.json +18 -0
@@ -0,0 +1,4 @@
1
+
2
+ > @sqrzro/auth@2.0.0-bz.0 build /Users/richard/Sites/@sqrzro/sqrzro/packages/auth
3
+ > tsc -p tsconfig.json
4
+
package/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Copyright 2024 Richard Carter
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4
+
5
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,33 @@
1
+ /// <reference types="react" />
2
+ import type { ClassNameProps } from '@sqrzro/components';
3
+ import type { ScopeObject } from '../../server';
4
+ export interface AuthClassNames {
5
+ actions: string;
6
+ footer: string;
7
+ link: string;
8
+ logo: string;
9
+ root: string;
10
+ panel: string;
11
+ title: string;
12
+ }
13
+ export interface AuthProps extends ClassNameProps<AuthClassNames> {
14
+ logo?: React.ReactElement;
15
+ onLogin?: () => Promise<void>;
16
+ params: {
17
+ auth: string[];
18
+ };
19
+ scopes?: Partial<ScopeObject>;
20
+ searchParams: {
21
+ email?: string;
22
+ r?: string;
23
+ token?: string;
24
+ };
25
+ }
26
+ /**
27
+ *
28
+ * A note on `classNames`: As `Auth` needs to run on the server (as some of its children require
29
+ * sessions etc), using the `getClassNames` pattern won't work, as it currently only works for
30
+ * client components. So we have to pass the classNames directly to the component.
31
+ */
32
+ declare function Auth({ classNames, logo, onLogin, params: { auth }, searchParams, }: Readonly<AuthProps>): Promise<React.ReactElement>;
33
+ export default Auth;
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { tw } from '@sqrzro/components';
3
+ import { notFound } from 'next/navigation';
4
+ import LoginForm from '../LoginForm';
5
+ import MFAPage from '../MFAPage';
6
+ import PasswordPage from '../PasswordPage';
7
+ import { registerAuthEvent } from '../../server';
8
+ /**
9
+ * To make it easier for consumers to use Auth in their projects, this auth component uses a
10
+ * Catch-All Segment to handle the rendering of all Auth pages. This function is used to determine
11
+ * which page to render based on the route.
12
+ *
13
+ * More information on Catch-All Segments can be found here:
14
+ * https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#catch-all-segments
15
+ *
16
+ * @param route
17
+ * @param props
18
+ * @returns
19
+ */
20
+ function getPage(route, props) {
21
+ switch (route) {
22
+ case 'mfa':
23
+ return _jsx(MFAPage, { ...props });
24
+ case 'password':
25
+ return _jsx(PasswordPage, { ...props });
26
+ default:
27
+ return _jsx(LoginForm, { ...props });
28
+ }
29
+ }
30
+ /**
31
+ *
32
+ * A note on `classNames`: As `Auth` needs to run on the server (as some of its children require
33
+ * sessions etc), using the `getClassNames` pattern won't work, as it currently only works for
34
+ * client components. So we have to pass the classNames directly to the component.
35
+ */
36
+ async function Auth({ classNames, logo, onLogin, params: { auth }, searchParams, }) {
37
+ if (auth.length > 1) {
38
+ return notFound();
39
+ }
40
+ await registerAuthEvent(onLogin);
41
+ return (_jsxs("div", { className: tw('grid h-screen grid-rows-[1fr_auto_auto_2fr] justify-center', classNames?.root), children: [_jsx("div", { className: tw('row-start-2', classNames?.logo), children: logo }), _jsx("div", { className: tw('row-start-3 w-screen max-w-sm', classNames?.panel), children: getPage(auth[0], {
42
+ classNames,
43
+ searchParams,
44
+ }) })] }));
45
+ }
46
+ export default Auth;
@@ -0,0 +1,10 @@
1
+ /// <reference types="react" />
2
+ import type { AuthClassNames } from '../Auth';
3
+ export interface LoginFormProps {
4
+ classNames?: Partial<AuthClassNames>;
5
+ searchParams: {
6
+ r?: string;
7
+ };
8
+ }
9
+ declare function LoginForm({ classNames, searchParams }: Readonly<LoginFormProps>): React.ReactElement;
10
+ export default LoginForm;
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Fragment } from 'react';
4
+ import { Form, FormSubmit, Link, PasswordFormField, TextFormField, tw } from '@sqrzro/components';
5
+ import { useLoginForm } from '@sqrzro/hooks';
6
+ import { submitLoginForm } from '../../server';
7
+ function LoginForm({ classNames, searchParams }) {
8
+ const { fieldProps, formData, formProps } = useLoginForm(submitLoginForm, {
9
+ redirect: searchParams.r,
10
+ });
11
+ return (_jsxs(Fragment, { children: [_jsx("h1", { className: tw('text-center', classNames?.title), children: "Sign in to continue" }), _jsxs(Form, { ...formProps, children: [_jsx(TextFormField, { ...fieldProps('email'), hasAssistiveError: true }), _jsx(PasswordFormField, { ...fieldProps('password'), hasAssistiveError: true }), _jsx("div", { className: tw('', classNames?.actions), children: _jsx(FormSubmit, { children: "Sign In" }) }), _jsx("footer", { className: tw('text-center', classNames?.footer), children: _jsx(Link, { className: classNames?.link, href: `/auth/password${formData.email ? `?email=${formData.email}` : ''}`, children: "Forgot Password?" }) })] })] }));
12
+ }
13
+ export default LoginForm;
@@ -0,0 +1,7 @@
1
+ /// <reference types="react" />
2
+ export interface LogoutButtonProps {
3
+ children?: React.ReactElement | string;
4
+ redirectTo?: string;
5
+ }
6
+ declare function LogoutButton({ children, redirectTo, }: Readonly<LogoutButtonProps>): React.ReactElement;
7
+ export default LogoutButton;
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useRouter } from 'next/navigation';
4
+ import { logout } from '../../server';
5
+ function LogoutButton({ children = 'Log out', redirectTo, }) {
6
+ const router = useRouter();
7
+ async function handleLogout() {
8
+ await logout();
9
+ router.push(redirectTo || '/');
10
+ }
11
+ return (_jsx("form", { action: handleLogout, className: "contents", children: _jsx("button", { type: "submit", children: children }) }));
12
+ }
13
+ export default LogoutButton;
@@ -0,0 +1,8 @@
1
+ /// <reference types="react" />
2
+ import type { AuthClassNames } from '../Auth';
3
+ export interface MFAFormProps {
4
+ classNames?: Partial<AuthClassNames>;
5
+ hasAssistiveSubmit?: boolean;
6
+ }
7
+ declare function MFAForm({ hasAssistiveSubmit }: Readonly<MFAFormProps>): React.ReactElement;
8
+ export default MFAForm;
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { CodeFormField, Form, FormSubmit, tw } from '@sqrzro/components';
5
+ import { useForm } from '@sqrzro/hooks';
6
+ import { useRouter } from 'next/navigation';
7
+ import { submitMFAForm } from '../../server';
8
+ function MFAForm({ hasAssistiveSubmit }) {
9
+ const router = useRouter();
10
+ const [errorCount, setErrorCount] = useState(0);
11
+ const { fieldProps, formProps, resetForm, submitForm } = useForm({
12
+ onSubmit: submitMFAForm,
13
+ onSuccess: () => {
14
+ router.push('/');
15
+ },
16
+ onValidationError: () => {
17
+ setErrorCount((count) => count + 1);
18
+ },
19
+ toasts: {
20
+ success: false,
21
+ validation: "There seems to be an issue with the code you've entered. Please try again.",
22
+ },
23
+ });
24
+ useEffect(() => {
25
+ resetForm();
26
+ }, [errorCount]);
27
+ return (_jsxs(Form, { ...formProps, children: [_jsx(CodeFormField, { ...fieldProps('token'), onFinalChange: submitForm, hasAssistiveLabel: true, isAutoFocus: true }, errorCount), _jsx("div", { className: tw(hasAssistiveSubmit ? 'sr-only' : ''), children: _jsx(FormSubmit, { children: "Verify" }) })] }));
28
+ }
29
+ export default MFAForm;
@@ -0,0 +1,7 @@
1
+ /// <reference types="react" />
2
+ import type { AuthClassNames } from '../Auth';
3
+ export interface MFAPageProps {
4
+ classNames?: Partial<AuthClassNames>;
5
+ }
6
+ declare function MFAPage({ classNames }: Readonly<MFAPageProps>): Promise<React.ReactElement>;
7
+ export default MFAPage;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { tw } from '@sqrzro/components';
3
+ import { notFound } from 'next/navigation';
4
+ import MFAForm from '../MFAForm';
5
+ import MFASetup from '../MFASetup';
6
+ import { checkMFAEnabled, checkUserHasMFA, getSessionUser } from '../../server';
7
+ async function MFAPage({ classNames }) {
8
+ if (!(await checkMFAEnabled())) {
9
+ return notFound();
10
+ }
11
+ const user = await getSessionUser();
12
+ if (!user) {
13
+ return _jsx("div", { children: "Error" });
14
+ }
15
+ const userHasMFA = await checkUserHasMFA(user);
16
+ if (!userHasMFA) {
17
+ return _jsx(MFASetup, { classNames: classNames });
18
+ }
19
+ return (_jsxs("div", { children: [_jsx("h1", { className: tw('text-center', classNames?.title), children: "Multi-Factor Authentication" }), _jsx("p", { className: "mb-6 text-center", children: "Enter the 6-digit code listed in your authentication app:" }), _jsx(MFAForm, { classNames: classNames, hasAssistiveSubmit: true })] }));
20
+ }
21
+ export default MFAPage;
@@ -0,0 +1,7 @@
1
+ /// <reference types="react" />
2
+ import type { AuthClassNames } from '../Auth';
3
+ export interface MFASetupProps {
4
+ classNames?: Partial<AuthClassNames>;
5
+ }
6
+ declare function MFASetup({ classNames }: Readonly<MFASetupProps>): Promise<React.ReactElement>;
7
+ export default MFASetup;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment } from 'react';
3
+ import { tw } from '@sqrzro/components';
4
+ import { generateMFA, getSessionUser } from '../../server';
5
+ import MFASetupForm from '../MFASetupForm';
6
+ async function MFASetup({ classNames }) {
7
+ if (!process.env.APP_NAME) {
8
+ throw new Error('MFA secret could not be generated because APP_NAME is not defined');
9
+ }
10
+ const user = await getSessionUser();
11
+ if (!user) {
12
+ return _jsx("div", { children: "Error" });
13
+ }
14
+ const url = await generateMFA(process.env.APP_NAME, user.email);
15
+ if (!url) {
16
+ return _jsx("div", { children: "Error" });
17
+ }
18
+ return (_jsxs(Fragment, { children: [_jsx("h1", { className: tw('', classNames?.title), children: "Multi-Factor Authentication" }), _jsx(MFASetupForm, { classNames: classNames, url: url })] }));
19
+ }
20
+ export default MFASetup;
@@ -0,0 +1,8 @@
1
+ /// <reference types="react" />
2
+ import type { AuthClassNames } from '../Auth';
3
+ export interface MFASetupProps {
4
+ classNames?: Partial<AuthClassNames>;
5
+ url: string;
6
+ }
7
+ declare function MFASetupForm({ classNames, url }: Readonly<MFASetupProps>): React.ReactElement;
8
+ export default MFASetupForm;
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Fragment, useState } from 'react';
4
+ import { Button, Link } from '@sqrzro/components';
5
+ import MFAForm from '../MFAForm';
6
+ function MFASetupForm({ classNames, url }) {
7
+ const [step, setStep] = useState(1);
8
+ function nextStep() {
9
+ setStep(2);
10
+ }
11
+ function previousStep(event) {
12
+ event.preventDefault();
13
+ setStep(1);
14
+ }
15
+ if (step === 1) {
16
+ return (_jsxs(Fragment, { children: [_jsx("p", { className: "mb-4", children: "You'll need to setup an additional authentication step before you can continue." }), _jsx("figure", { className: "flex justify-center rounded border p-2", children: _jsx("img", { alt: "qrcode", src: url }) }), _jsxs("p", { className: "mb-4", children: [_jsx("strong", { children: "Step 1:" }), " Scan the QR code, or enter the secret below, in the Google Authentictor app."] }), _jsx(Button, { onClick: nextStep, variant: "primary", isFullWidth: true, children: "Continue" })] }));
17
+ }
18
+ return (_jsxs(Fragment, { children: [_jsxs("p", { className: "mb-4", children: [_jsx("strong", { children: "Step 2:" }), " Enter the 6-digit code listed in the Google Authenticator app:"] }), _jsx(MFAForm, {}), _jsx(Link, { className: classNames?.link, onClick: previousStep, children: "Scan QRcode again" })] }));
19
+ }
20
+ export default MFASetupForm;
@@ -0,0 +1,8 @@
1
+ /// <reference types="react" />
2
+ import type { AuthClassNames } from '../Auth';
3
+ interface PasswordFormProps {
4
+ classNames?: Partial<AuthClassNames>;
5
+ email?: string;
6
+ }
7
+ declare function PasswordForm({ classNames, email }: Readonly<PasswordFormProps>): React.ReactElement;
8
+ export default PasswordForm;
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Fragment, useState } from 'react';
4
+ import { Form, FormSubmit, Link, TextFormField, tw } from '@sqrzro/components';
5
+ import { useForm } from '@sqrzro/hooks';
6
+ import { submitPasswordForm } from '../../server';
7
+ function PasswordForm({ classNames, email }) {
8
+ const [sentCount, setSentCount] = useState(0);
9
+ const { fieldProps, formData, formProps, setFormData, submitForm } = useForm({
10
+ defaults: { email },
11
+ onSubmit: submitPasswordForm,
12
+ onSuccess: () => {
13
+ setSentCount(sentCount + 1);
14
+ },
15
+ toasts: { success: false },
16
+ });
17
+ function handleResend(event) {
18
+ event.preventDefault();
19
+ submitForm();
20
+ }
21
+ function handleReset(event) {
22
+ event.preventDefault();
23
+ setFormData('email', '');
24
+ setSentCount(0);
25
+ }
26
+ return (_jsxs(Fragment, { children: [_jsx("div", { className: tw('', sentCount === 0 ? 'block' : 'hidden'), children: _jsxs(Form, { ...formProps, children: [_jsxs("div", { 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'), hasAssistiveLabel: true }), _jsx("div", { className: tw('', classNames?.actions), children: _jsx(FormSubmit, { children: "Send Email" }) })] }) }), _jsxs("div", { className: tw('-mt-1 text-sm', sentCount > 0 ? 'block' : 'hidden'), children: [_jsx("h1", { className: classNames?.title, children: "Check Your Email" }), sentCount === 1 ? (_jsxs(Fragment, { children: [_jsxs("p", { className: "mb-4", children: ["If ", 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", { 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: "mb-4", children: ["We've resent password reset instructions to ", formData.email, ", if it is an email we have on file."] }), _jsxs("p", { 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" }), "."] })] }))] })] }));
27
+ }
28
+ export default PasswordForm;
@@ -0,0 +1,11 @@
1
+ /// <reference types="react" />
2
+ import type { AuthClassNames } from '../Auth';
3
+ interface PasswordPageProps {
4
+ classNames?: Partial<AuthClassNames>;
5
+ searchParams: {
6
+ email?: string;
7
+ token?: string;
8
+ };
9
+ }
10
+ declare function PasswordPage({ classNames, searchParams, }: Readonly<PasswordPageProps>): React.ReactElement;
11
+ export default PasswordPage;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import PasswordForm from '../PasswordForm';
3
+ import PasswordResetForm from '../PasswordResetForm';
4
+ function PasswordPage({ classNames, searchParams, }) {
5
+ if (searchParams.token) {
6
+ return _jsx(PasswordResetForm, { classNames: classNames, token: searchParams.token });
7
+ }
8
+ return _jsx(PasswordForm, { classNames: classNames, email: searchParams.email });
9
+ }
10
+ export default PasswordPage;
@@ -0,0 +1,8 @@
1
+ /// <reference types="react" />
2
+ import type { AuthClassNames } from '../Auth';
3
+ interface PasswordResetFormProps {
4
+ classNames?: Partial<AuthClassNames>;
5
+ token: string;
6
+ }
7
+ declare function PasswordResetForm({ classNames, token, }: Readonly<PasswordResetFormProps>): React.ReactElement;
8
+ export default PasswordResetForm;
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Form, FormSubmit, PasswordFormField } from '@sqrzro/components';
4
+ import { useForm } from '@sqrzro/hooks';
5
+ import { useRouter } from 'next/navigation';
6
+ import { submitPasswordResetForm } from '../../server';
7
+ function PasswordResetForm({ classNames, token, }) {
8
+ const router = useRouter();
9
+ const { fieldProps, formProps } = useForm({
10
+ defaults: { token },
11
+ hiddenFields: ['token'],
12
+ onSubmit: submitPasswordResetForm,
13
+ onSuccess: (response) => {
14
+ router.push(response || '/');
15
+ },
16
+ toasts: { success: false },
17
+ });
18
+ return (_jsxs(Form, { ...formProps, children: [_jsx("h1", { className: classNames?.title, children: "Reset Your Password" }), _jsx("div", { className: "relative", children: _jsx(PasswordFormField, { ...fieldProps('password', 'New Password') }) }), _jsx("div", { className: "mt-8", children: _jsx(FormSubmit, { children: "Reset Password" }) })] }));
19
+ }
20
+ export default PasswordResetForm;
@@ -0,0 +1,3 @@
1
+ export { default as Auth } from './components/Auth';
2
+ export type { AuthProps } from './components/Auth';
3
+ export { default as LogoutButton } from './components/LogoutButton';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { default as Auth } from './components/Auth';
2
+ export { default as LogoutButton } from './components/LogoutButton';
@@ -0,0 +1,20 @@
1
+ import type { LoginFormFields, MFAFormFields, UserObject } from '@sqrzro/server/auth';
2
+ import type { Errorable } from '@sqrzro/interfaces';
3
+ export type { MFAFormFields, ScopeObject } from '@sqrzro/server/auth';
4
+ export declare function registerAuthEvent(fn?: () => Promise<void>): Promise<void>;
5
+ export declare function checkUserHasMFA(user: UserObject): Promise<boolean>;
6
+ export declare function generateMFA(name: string, email?: string): Promise<string | null>;
7
+ export declare function getSessionUser(): Promise<UserObject | null>;
8
+ export declare function checkMFAEnabled(): Promise<boolean>;
9
+ interface PasswordFormFields {
10
+ email: string;
11
+ }
12
+ interface PasswordResetFormFields {
13
+ password: string;
14
+ token: string;
15
+ }
16
+ export declare function submitLoginForm(formData: LoginFormFields): Promise<Errorable<string>>;
17
+ export declare function submitMFAForm(formData: MFAFormFields): Promise<Errorable<boolean>>;
18
+ export declare function submitPasswordForm(formData: PasswordFormFields): Promise<Errorable<boolean>>;
19
+ export declare function submitPasswordResetForm(formData: PasswordResetFormFields): Promise<Errorable<string | null>>;
20
+ export declare function logout(): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,46 @@
1
+ 'use server';
2
+ import { handleLoginForm, handleLogout, handleMFAForm, handlePasswordForm, handlePasswordResetForm, checkUserHasMFA as serverCheckUserHasMFA, generateMFA as serverGenerateMFA, getSessionUser as serverGetSessionUser, checkMFAEnabled as syncCheckMFAEnabled, } from '@sqrzro/server/auth';
3
+ let authEvent; // eslint-disable-line @typescript-eslint/init-declarations
4
+ export async function registerAuthEvent(fn) {
5
+ if (fn) {
6
+ authEvent = fn;
7
+ }
8
+ return Promise.resolve();
9
+ }
10
+ /*
11
+ * Functions re-exported from @sqrzro/server/auth
12
+ *
13
+ * NextJS may complain about these functions not being async if they are exported directly from the
14
+ * @sqrzro/server/auth module, so we re-export them here to be on the safe side.
15
+ */
16
+ export async function checkUserHasMFA(user) {
17
+ return serverCheckUserHasMFA(user);
18
+ }
19
+ export async function generateMFA(name, email) {
20
+ return serverGenerateMFA(name, email);
21
+ }
22
+ export async function getSessionUser() {
23
+ return serverGetSessionUser();
24
+ }
25
+ // This function is synchronous, so we need to wrap it in a promise so it can be exported properly.
26
+ export async function checkMFAEnabled() {
27
+ return Promise.resolve(syncCheckMFAEnabled());
28
+ }
29
+ export async function submitLoginForm(formData) {
30
+ return handleLoginForm(formData, authEvent);
31
+ }
32
+ export async function submitMFAForm(formData) {
33
+ return handleMFAForm(formData);
34
+ }
35
+ async function sendPasswordResetMail(email, token) {
36
+ return true;
37
+ }
38
+ export async function submitPasswordForm(formData) {
39
+ return handlePasswordForm(formData, sendPasswordResetMail);
40
+ }
41
+ export async function submitPasswordResetForm(formData) {
42
+ return handlePasswordResetForm(formData);
43
+ }
44
+ export async function logout() {
45
+ return handleLogout();
46
+ }
package/next-env.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/basic-features/typescript for more information.
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@sqrzro/auth",
3
+ "type": "module",
4
+ "version": "2.0.0-bz.0",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "dependencies": {
8
+ "@sqrzro/components": "bz",
9
+ "@sqrzro/hooks": "bz",
10
+ "@sqrzro/interfaces": "bz",
11
+ "@sqrzro/server": "bz",
12
+ "next": "^14.1.4",
13
+ "react": "^18.2.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/react": "^18.2.74",
17
+ "typescript": "^5.4.4"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json"
21
+ }
22
+ }
@@ -0,0 +1 @@
1
+ module.exports = require('@sqrzro/prettier-config');
@@ -0,0 +1,95 @@
1
+ import { tw } from '@sqrzro/components';
2
+ import type { ClassNameProps } from '@sqrzro/components';
3
+ import { notFound } from 'next/navigation';
4
+
5
+ import LoginForm from '../LoginForm';
6
+ import MFAPage from '../MFAPage';
7
+ import PasswordPage from '../PasswordPage';
8
+
9
+ import { registerAuthEvent } from '../../server';
10
+ import type { ScopeObject } from '../../server';
11
+
12
+ export interface AuthClassNames {
13
+ actions: string;
14
+ footer: string;
15
+ link: string;
16
+ logo: string;
17
+ root: string;
18
+ panel: string;
19
+ title: string;
20
+ }
21
+
22
+ export interface AuthProps extends ClassNameProps<AuthClassNames> {
23
+ logo?: React.ReactElement;
24
+ onLogin?: () => Promise<void>;
25
+ params: { auth: string[] };
26
+ scopes?: Partial<ScopeObject>;
27
+ searchParams: { email?: string; r?: string; token?: string };
28
+ }
29
+
30
+ interface AuthPageProps {
31
+ classNames?: Partial<AuthClassNames>;
32
+ searchParams: AuthProps['searchParams'];
33
+ }
34
+
35
+ /**
36
+ * To make it easier for consumers to use Auth in their projects, this auth component uses a
37
+ * Catch-All Segment to handle the rendering of all Auth pages. This function is used to determine
38
+ * which page to render based on the route.
39
+ *
40
+ * More information on Catch-All Segments can be found here:
41
+ * https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#catch-all-segments
42
+ *
43
+ * @param route
44
+ * @param props
45
+ * @returns
46
+ */
47
+ function getPage(route: string, props: AuthPageProps): React.ReactElement {
48
+ switch (route) {
49
+ case 'mfa':
50
+ return <MFAPage {...props} />;
51
+ case 'password':
52
+ return <PasswordPage {...props} />;
53
+ default:
54
+ return <LoginForm {...props} />;
55
+ }
56
+ }
57
+
58
+ /**
59
+ *
60
+ * A note on `classNames`: As `Auth` needs to run on the server (as some of its children require
61
+ * sessions etc), using the `getClassNames` pattern won't work, as it currently only works for
62
+ * client components. So we have to pass the classNames directly to the component.
63
+ */
64
+ async function Auth({
65
+ classNames,
66
+ logo,
67
+ onLogin,
68
+ params: { auth },
69
+ searchParams,
70
+ }: Readonly<AuthProps>): Promise<React.ReactElement> {
71
+ if (auth.length > 1) {
72
+ return notFound();
73
+ }
74
+
75
+ await registerAuthEvent(onLogin);
76
+
77
+ return (
78
+ <div
79
+ className={tw(
80
+ 'grid h-screen grid-rows-[1fr_auto_auto_2fr] justify-center',
81
+ classNames?.root
82
+ )}
83
+ >
84
+ <div className={tw('row-start-2', classNames?.logo)}>{logo}</div>
85
+ <div className={tw('row-start-3 w-screen max-w-sm', classNames?.panel)}>
86
+ {getPage(auth[0], {
87
+ classNames,
88
+ searchParams,
89
+ })}
90
+ </div>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ export default Auth;
@@ -0,0 +1,44 @@
1
+ 'use client';
2
+
3
+ import { Fragment } from 'react';
4
+
5
+ import { Form, FormSubmit, Link, PasswordFormField, TextFormField, tw } from '@sqrzro/components';
6
+ import { useLoginForm } from '@sqrzro/hooks';
7
+
8
+ import { submitLoginForm } from '../../server';
9
+
10
+ import type { AuthClassNames } from '../Auth';
11
+
12
+ export interface LoginFormProps {
13
+ classNames?: Partial<AuthClassNames>;
14
+ searchParams: { r?: string };
15
+ }
16
+
17
+ function LoginForm({ classNames, searchParams }: Readonly<LoginFormProps>): React.ReactElement {
18
+ const { fieldProps, formData, formProps } = useLoginForm(submitLoginForm, {
19
+ redirect: searchParams.r,
20
+ });
21
+
22
+ return (
23
+ <Fragment>
24
+ <h1 className={tw('text-center', classNames?.title)}>Sign in to continue</h1>
25
+ <Form {...formProps}>
26
+ <TextFormField {...fieldProps('email')} hasAssistiveError />
27
+ <PasswordFormField {...fieldProps('password')} hasAssistiveError />
28
+ <div className={tw('', classNames?.actions)}>
29
+ <FormSubmit>Sign In</FormSubmit>
30
+ </div>
31
+ <footer className={tw('text-center', classNames?.footer)}>
32
+ <Link
33
+ className={classNames?.link}
34
+ href={`/auth/password${formData.email ? `?email=${formData.email}` : ''}`}
35
+ >
36
+ Forgot Password?
37
+ </Link>
38
+ </footer>
39
+ </Form>
40
+ </Fragment>
41
+ );
42
+ }
43
+
44
+ export default LoginForm;
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+
3
+ import { useRouter } from 'next/navigation';
4
+
5
+ import { logout } from '../../server';
6
+
7
+ export interface LogoutButtonProps {
8
+ children?: React.ReactElement | string;
9
+ redirectTo?: string;
10
+ }
11
+
12
+ function LogoutButton({
13
+ children = 'Log out',
14
+ redirectTo,
15
+ }: Readonly<LogoutButtonProps>): React.ReactElement {
16
+ const router = useRouter();
17
+
18
+ async function handleLogout(): Promise<void> {
19
+ await logout();
20
+ router.push(redirectTo || '/');
21
+ }
22
+
23
+ return (
24
+ <form action={handleLogout} className="contents">
25
+ <button type="submit">{children}</button>
26
+ </form>
27
+ );
28
+ }
29
+
30
+ export default LogoutButton;
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ import { CodeFormField, Form, FormSubmit, tw } from '@sqrzro/components';
6
+ import { useForm } from '@sqrzro/hooks';
7
+ import { useRouter } from 'next/navigation';
8
+
9
+ import { submitMFAForm } from '../../server';
10
+ import type { MFAFormFields } from '../../server';
11
+
12
+ import type { AuthClassNames } from '../Auth';
13
+
14
+ export interface MFAFormProps {
15
+ classNames?: Partial<AuthClassNames>;
16
+ hasAssistiveSubmit?: boolean;
17
+ }
18
+
19
+ function MFAForm({ hasAssistiveSubmit }: Readonly<MFAFormProps>): React.ReactElement {
20
+ const router = useRouter();
21
+
22
+ const [errorCount, setErrorCount] = useState(0);
23
+
24
+ const { fieldProps, formProps, resetForm, submitForm } = useForm<MFAFormFields, boolean>({
25
+ onSubmit: submitMFAForm,
26
+ onSuccess: () => {
27
+ router.push('/');
28
+ },
29
+ onValidationError: () => {
30
+ setErrorCount((count) => count + 1);
31
+ },
32
+ toasts: {
33
+ success: false,
34
+ validation:
35
+ "There seems to be an issue with the code you've entered. Please try again.",
36
+ },
37
+ });
38
+
39
+ useEffect(() => {
40
+ resetForm();
41
+ }, [errorCount]);
42
+
43
+ return (
44
+ <Form {...formProps}>
45
+ <CodeFormField
46
+ key={errorCount}
47
+ {...fieldProps('token')}
48
+ onFinalChange={submitForm}
49
+ hasAssistiveLabel
50
+ isAutoFocus
51
+ />
52
+ <div className={tw(hasAssistiveSubmit ? 'sr-only' : '')}>
53
+ <FormSubmit>Verify</FormSubmit>
54
+ </div>
55
+ </Form>
56
+ );
57
+ }
58
+
59
+ export default MFAForm;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ var components_1 = require("@sqrzro/components");
40
+ var navigation_1 = require("next/navigation");
41
+ var MFAForm_1 = require("../MFAForm");
42
+ var MFASetup_1 = require("../MFASetup");
43
+ var server_1 = require("../../server");
44
+ function MFAPage(_a) {
45
+ return __awaiter(this, arguments, void 0, function (_b) {
46
+ var user, userHasMFA;
47
+ var classNames = _b.classNames;
48
+ return __generator(this, function (_c) {
49
+ switch (_c.label) {
50
+ case 0: return [4 /*yield*/, (0, server_1.checkMFAEnabled)()];
51
+ case 1:
52
+ if (!(_c.sent())) {
53
+ return [2 /*return*/, (0, navigation_1.notFound)()];
54
+ }
55
+ return [4 /*yield*/, (0, server_1.getSessionUser)()];
56
+ case 2:
57
+ user = _c.sent();
58
+ if (!user) {
59
+ return [2 /*return*/, <div>Error</div>];
60
+ }
61
+ return [4 /*yield*/, (0, server_1.checkUserHasMFA)(user)];
62
+ case 3:
63
+ userHasMFA = _c.sent();
64
+ if (!userHasMFA) {
65
+ return [2 /*return*/, <MFASetup_1.default classNames={classNames}/>];
66
+ }
67
+ return [2 /*return*/, (<div>
68
+ <h1 className={(0, components_1.tw)('text-center', classNames === null || classNames === void 0 ? void 0 : classNames.title)}>Multi-Factor Authentication</h1>
69
+ <p className="mb-6 text-center">
70
+ Enter the 6-digit code listed in your authentication app:
71
+ </p>
72
+ <MFAForm_1.default classNames={classNames} hasAssistiveSubmit/>
73
+ </div>)];
74
+ }
75
+ });
76
+ });
77
+ }
78
+ exports.default = MFAPage;
@@ -0,0 +1,42 @@
1
+ import { tw } from '@sqrzro/components';
2
+ import { notFound } from 'next/navigation';
3
+
4
+ import type { AuthClassNames } from '../Auth';
5
+ import MFAForm from '../MFAForm';
6
+ import MFASetup from '../MFASetup';
7
+
8
+ import { checkMFAEnabled, checkUserHasMFA, getSessionUser } from '../../server';
9
+
10
+ export interface MFAPageProps {
11
+ classNames?: Partial<AuthClassNames>;
12
+ }
13
+
14
+ async function MFAPage({ classNames }: Readonly<MFAPageProps>): Promise<React.ReactElement> {
15
+ if (!(await checkMFAEnabled())) {
16
+ return notFound();
17
+ }
18
+
19
+ const user = await getSessionUser();
20
+
21
+ if (!user) {
22
+ return <div>Error</div>;
23
+ }
24
+
25
+ const userHasMFA = await checkUserHasMFA(user);
26
+
27
+ if (!userHasMFA) {
28
+ return <MFASetup classNames={classNames} />;
29
+ }
30
+
31
+ return (
32
+ <div>
33
+ <h1 className={tw('text-center', classNames?.title)}>Multi-Factor Authentication</h1>
34
+ <p className="mb-6 text-center">
35
+ Enter the 6-digit code listed in your authentication app:
36
+ </p>
37
+ <MFAForm classNames={classNames} hasAssistiveSubmit />
38
+ </div>
39
+ );
40
+ }
41
+
42
+ export default MFAPage;
@@ -0,0 +1,39 @@
1
+ import { Fragment } from 'react';
2
+
3
+ import { tw } from '@sqrzro/components';
4
+
5
+ import { generateMFA, getSessionUser } from '../../server';
6
+
7
+ import type { AuthClassNames } from '../Auth';
8
+ import MFASetupForm from '../MFASetupForm';
9
+
10
+ export interface MFASetupProps {
11
+ classNames?: Partial<AuthClassNames>;
12
+ }
13
+
14
+ async function MFASetup({ classNames }: Readonly<MFASetupProps>): Promise<React.ReactElement> {
15
+ if (!process.env.APP_NAME) {
16
+ throw new Error('MFA secret could not be generated because APP_NAME is not defined');
17
+ }
18
+
19
+ const user = await getSessionUser();
20
+
21
+ if (!user) {
22
+ return <div>Error</div>;
23
+ }
24
+
25
+ const url = await generateMFA(process.env.APP_NAME, user.email);
26
+
27
+ if (!url) {
28
+ return <div>Error</div>;
29
+ }
30
+
31
+ return (
32
+ <Fragment>
33
+ <h1 className={tw('', classNames?.title)}>Multi-Factor Authentication</h1>
34
+ <MFASetupForm classNames={classNames} url={url} />
35
+ </Fragment>
36
+ );
37
+ }
38
+
39
+ export default MFASetup;
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+
3
+ import { Fragment, useState } from 'react';
4
+
5
+ import { Button, Link } from '@sqrzro/components';
6
+
7
+ import type { AuthClassNames } from '../Auth';
8
+ import MFAForm from '../MFAForm';
9
+
10
+ export interface MFASetupProps {
11
+ classNames?: Partial<AuthClassNames>;
12
+ url: string;
13
+ }
14
+
15
+ function MFASetupForm({ classNames, url }: Readonly<MFASetupProps>): React.ReactElement {
16
+ const [step, setStep] = useState(1);
17
+
18
+ function nextStep(): void {
19
+ setStep(2);
20
+ }
21
+
22
+ function previousStep(event: React.MouseEvent): void {
23
+ event.preventDefault();
24
+ setStep(1);
25
+ }
26
+
27
+ if (step === 1) {
28
+ return (
29
+ <Fragment>
30
+ <p className="mb-4">
31
+ You&#39;ll need to setup an additional authentication step before you can
32
+ continue.
33
+ </p>
34
+ <figure className="flex justify-center rounded border p-2">
35
+ <img alt="qrcode" src={url} />
36
+ </figure>
37
+ <p className="mb-4">
38
+ <strong>Step 1:</strong> Scan the QR code, or enter the secret below, in the
39
+ Google Authentictor app.
40
+ </p>
41
+ <Button onClick={nextStep} variant="primary" isFullWidth>
42
+ Continue
43
+ </Button>
44
+ </Fragment>
45
+ );
46
+ }
47
+
48
+ return (
49
+ <Fragment>
50
+ <p className="mb-4">
51
+ <strong>Step 2:</strong> Enter the 6-digit code listed in the Google Authenticator
52
+ app:
53
+ </p>
54
+ <MFAForm />
55
+ <Link className={classNames?.link} onClick={previousStep}>
56
+ Scan QRcode again
57
+ </Link>
58
+ </Fragment>
59
+ );
60
+ }
61
+
62
+ export default MFASetupForm;
@@ -0,0 +1,107 @@
1
+ 'use client';
2
+
3
+ import { Fragment, useState } from 'react';
4
+
5
+ import { Form, FormSubmit, Link, TextFormField, tw } from '@sqrzro/components';
6
+ import { useForm } from '@sqrzro/hooks';
7
+
8
+ import { submitPasswordForm } from '../../server';
9
+
10
+ import type { AuthClassNames } from '../Auth';
11
+
12
+ interface PasswordFormFields {
13
+ email: string;
14
+ }
15
+
16
+ interface PasswordFormProps {
17
+ classNames?: Partial<AuthClassNames>;
18
+ email?: string;
19
+ }
20
+
21
+ function PasswordForm({ classNames, email }: Readonly<PasswordFormProps>): React.ReactElement {
22
+ const [sentCount, setSentCount] = useState(0);
23
+
24
+ const { fieldProps, formData, formProps, setFormData, submitForm } = useForm<
25
+ PasswordFormFields,
26
+ boolean
27
+ >({
28
+ defaults: { email },
29
+ onSubmit: submitPasswordForm,
30
+ onSuccess: () => {
31
+ setSentCount(sentCount + 1);
32
+ },
33
+ toasts: { success: false },
34
+ });
35
+
36
+ function handleResend(event: React.MouseEvent<HTMLAnchorElement>): void {
37
+ event.preventDefault();
38
+ submitForm();
39
+ }
40
+
41
+ function handleReset(event: React.MouseEvent<HTMLAnchorElement>): void {
42
+ event.preventDefault();
43
+
44
+ setFormData('email', '');
45
+ setSentCount(0);
46
+ }
47
+
48
+ return (
49
+ <Fragment>
50
+ <div className={tw('', sentCount === 0 ? 'block' : 'hidden')}>
51
+ <Form {...formProps}>
52
+ <div>
53
+ <h1 className={classNames?.title}>Reset Your Password</h1>
54
+ <p className="text-sm">
55
+ Enter the email address associated with your account and we&#39;ll send
56
+ you a link to reset your password.
57
+ </p>
58
+ </div>
59
+ <TextFormField {...fieldProps('email')} hasAssistiveLabel />
60
+ <div className={tw('', classNames?.actions)}>
61
+ <FormSubmit>Send Email</FormSubmit>
62
+ </div>
63
+ </Form>
64
+ </div>
65
+
66
+ <div className={tw('-mt-1 text-sm', sentCount > 0 ? 'block' : 'hidden')}>
67
+ <h1 className={classNames?.title}>Check Your Email</h1>
68
+ {sentCount === 1 ? (
69
+ <Fragment>
70
+ <p className="mb-4">
71
+ If {formData.email} matches an email we have on file, then we&#39;ve
72
+ sent you an email containing further instructions for resetting your
73
+ password.
74
+ </p>
75
+ <p>
76
+ If you haven&#39;t received an email in 5 minutes, check your spam,{' '}
77
+ <Link className={classNames?.link} onClick={handleResend}>
78
+ resend
79
+ </Link>
80
+ , or{' '}
81
+ <Link className={classNames?.link} onClick={handleReset}>
82
+ try a different email address
83
+ </Link>
84
+ .
85
+ </p>
86
+ </Fragment>
87
+ ) : (
88
+ <Fragment>
89
+ <p className="mb-4">
90
+ We&#39;ve resent password reset instructions to {formData.email}, if it
91
+ is an email we have on file.
92
+ </p>
93
+ <p>
94
+ Please check again. If you still haven&#39;t received an email,{' '}
95
+ <Link className={classNames?.link} onClick={handleReset}>
96
+ try a different email address
97
+ </Link>
98
+ .
99
+ </p>
100
+ </Fragment>
101
+ )}
102
+ </div>
103
+ </Fragment>
104
+ );
105
+ }
106
+
107
+ export default PasswordForm;
@@ -0,0 +1,20 @@
1
+ import type { AuthClassNames } from '../Auth';
2
+ import PasswordForm from '../PasswordForm';
3
+ import PasswordResetForm from '../PasswordResetForm';
4
+
5
+ interface PasswordPageProps {
6
+ classNames?: Partial<AuthClassNames>;
7
+ searchParams: { email?: string; token?: string };
8
+ }
9
+
10
+ function PasswordPage({
11
+ classNames,
12
+ searchParams,
13
+ }: Readonly<PasswordPageProps>): React.ReactElement {
14
+ if (searchParams.token) {
15
+ return <PasswordResetForm classNames={classNames} token={searchParams.token} />;
16
+ }
17
+ return <PasswordForm classNames={classNames} email={searchParams.email} />;
18
+ }
19
+
20
+ export default PasswordPage;
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+
3
+ import { Form, FormSubmit, PasswordFormField } from '@sqrzro/components';
4
+ import { useForm } from '@sqrzro/hooks';
5
+ import { useRouter } from 'next/navigation';
6
+
7
+ import { submitPasswordResetForm } from '../../server';
8
+
9
+ import type { AuthClassNames } from '../Auth';
10
+
11
+ interface PasswordResetFormFields {
12
+ password: string;
13
+ token: string;
14
+ }
15
+
16
+ interface PasswordResetFormProps {
17
+ classNames?: Partial<AuthClassNames>;
18
+ token: string;
19
+ }
20
+
21
+ function PasswordResetForm({
22
+ classNames,
23
+ token,
24
+ }: Readonly<PasswordResetFormProps>): React.ReactElement {
25
+ const router = useRouter();
26
+
27
+ const { fieldProps, formProps } = useForm<PasswordResetFormFields, string | null>({
28
+ defaults: { token },
29
+ hiddenFields: ['token'],
30
+ onSubmit: submitPasswordResetForm,
31
+ onSuccess: (response) => {
32
+ router.push(response || '/');
33
+ },
34
+ toasts: { success: false },
35
+ });
36
+
37
+ return (
38
+ <Form {...formProps}>
39
+ <h1 className={classNames?.title}>Reset Your Password</h1>
40
+ <div className="relative">
41
+ <PasswordFormField {...fieldProps('password', 'New Password')} />
42
+ </div>
43
+ <div className="mt-8">
44
+ <FormSubmit>Reset Password</FormSubmit>
45
+ </div>
46
+ </Form>
47
+ );
48
+ }
49
+
50
+ export default PasswordResetForm;
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { default as Auth } from './components/Auth';
2
+ export type { AuthProps } from './components/Auth';
3
+ export { default as LogoutButton } from './components/LogoutButton';
package/src/server.ts ADDED
@@ -0,0 +1,87 @@
1
+ 'use server';
2
+
3
+ import {
4
+ handleLoginForm,
5
+ handleLogout,
6
+ handleMFAForm,
7
+ handlePasswordForm,
8
+ handlePasswordResetForm,
9
+ checkUserHasMFA as serverCheckUserHasMFA,
10
+ generateMFA as serverGenerateMFA,
11
+ getSessionUser as serverGetSessionUser,
12
+ checkMFAEnabled as syncCheckMFAEnabled,
13
+ } from '@sqrzro/server/auth';
14
+ import type { LoginFormFields, MFAFormFields, UserObject } from '@sqrzro/server/auth';
15
+ import type { Errorable } from '@sqrzro/interfaces';
16
+
17
+ export type { MFAFormFields, ScopeObject } from '@sqrzro/server/auth';
18
+
19
+ let authEvent: () => Promise<void>; // eslint-disable-line @typescript-eslint/init-declarations
20
+
21
+ export async function registerAuthEvent(fn?: () => Promise<void>): Promise<void> {
22
+ if (fn) {
23
+ authEvent = fn;
24
+ }
25
+ return Promise.resolve();
26
+ }
27
+
28
+ /*
29
+ * Functions re-exported from @sqrzro/server/auth
30
+ *
31
+ * NextJS may complain about these functions not being async if they are exported directly from the
32
+ * @sqrzro/server/auth module, so we re-export them here to be on the safe side.
33
+ */
34
+
35
+ export async function checkUserHasMFA(user: UserObject): Promise<boolean> {
36
+ return serverCheckUserHasMFA(user);
37
+ }
38
+
39
+ export async function generateMFA(name: string, email?: string): Promise<string | null> {
40
+ return serverGenerateMFA(name, email);
41
+ }
42
+
43
+ export async function getSessionUser(): Promise<UserObject | null> {
44
+ return serverGetSessionUser();
45
+ }
46
+
47
+ // This function is synchronous, so we need to wrap it in a promise so it can be exported properly.
48
+ export async function checkMFAEnabled(): Promise<boolean> {
49
+ return Promise.resolve(syncCheckMFAEnabled());
50
+ }
51
+
52
+ interface PasswordFormFields {
53
+ email: string;
54
+ }
55
+
56
+ interface PasswordResetFormFields {
57
+ password: string;
58
+ token: string;
59
+ }
60
+
61
+ export async function submitLoginForm(formData: LoginFormFields): Promise<Errorable<string>> {
62
+ return handleLoginForm(formData, authEvent);
63
+ }
64
+
65
+ export async function submitMFAForm(formData: MFAFormFields): Promise<Errorable<boolean>> {
66
+ return handleMFAForm(formData);
67
+ }
68
+
69
+ async function sendPasswordResetMail(email: string, token: string): Promise<boolean> {
70
+ return true;
71
+ }
72
+
73
+ export async function submitPasswordForm(
74
+ formData: PasswordFormFields
75
+ ): Promise<Errorable<boolean>> {
76
+ return handlePasswordForm(formData, sendPasswordResetMail);
77
+ }
78
+
79
+ export async function submitPasswordResetForm(
80
+ formData: PasswordResetFormFields
81
+ ): Promise<Errorable<string | null>> {
82
+ return handlePasswordResetForm(formData);
83
+ }
84
+
85
+ export async function logout(): Promise<void> {
86
+ return handleLogout();
87
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowSyntheticDefaultImports": true,
4
+ "declaration": true,
5
+ "esModuleInterop": true,
6
+ "importHelpers": true,
7
+ "jsx": "react-jsx",
8
+ "lib": ["DOM", "ESNext"],
9
+ "module": "ESNext",
10
+ "moduleResolution": "node",
11
+ "outDir": "dist",
12
+ "resolveJsonModule": true,
13
+ "skipLibCheck": true,
14
+ "target": "ESNext"
15
+ },
16
+ "include": ["next-env.d.ts", "src/**/*"],
17
+ "exclude": ["node_modules"]
18
+ }