diff --git a/__fixtures__/test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx b/__fixtures__/test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx index 8750916e6a8e..8b383d5000ff 100644 --- a/__fixtures__/test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx +++ b/__fixtures__/test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx @@ -68,7 +68,10 @@ const ForgotPasswordPage = () => { errorClassName="rw-input rw-input-error" ref={usernameRef} validation={{ - required: true, + required: { + value: true, + message: 'Username is required', + }, }} /> diff --git a/__fixtures__/test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx b/__fixtures__/test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx index 31744940b706..cc640e4c1fcc 100644 --- a/__fixtures__/test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx +++ b/__fixtures__/test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx @@ -92,7 +92,7 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => { validation={{ required: { value: true, - message: 'Password is required', + message: 'New Password is required', }, }} /> diff --git a/__fixtures__/test-project/web/src/pages/SignupPage/SignupPage.tsx b/__fixtures__/test-project/web/src/pages/SignupPage/SignupPage.tsx index 195d13a94d61..6093d93cee2d 100644 --- a/__fixtures__/test-project/web/src/pages/SignupPage/SignupPage.tsx +++ b/__fixtures__/test-project/web/src/pages/SignupPage/SignupPage.tsx @@ -24,7 +24,7 @@ const SignupPage = () => { } }, [isAuthenticated]) - // focus on email box on page load + // focus on username box on page load const usernameRef = useRef(null) useEffect(() => { usernameRef.current?.focus() diff --git a/docs/docs/cli-commands.md b/docs/docs/cli-commands.md index 3642fa10b9f6..ad0835c04793 100644 --- a/docs/docs/cli-commands.md +++ b/docs/docs/cli-commands.md @@ -588,10 +588,12 @@ Generate log in, sign up, forgot password and password reset pages for dbAuth yarn redwood generate dbAuth ``` -| Arguments & Options | Description | -| ------------------- | ------------------------------------------------------------------------------------------------ | -| `--webAuthn` | Whether or not to add webAuthn support to the log in page. If not specified you will be prompted | -| `--rollback` | Rollback changes if an error occurs [default: true] | +| Arguments & Options | Description | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `--username-label` | The label to give the username field on the auth forms, e.g. "Email". Defaults to "Username". If not specified you will be prompted | +| `--password-label` | The label to give the password field on the auth forms, e.g. "Secret". Defaults to "Password". If not specified you will be prompted | +| `--webAuthn` | Whether or not to add webAuthn support to the log in page. If not specified you will be prompted | +| `--rollback` | Rollback changes if an error occurs [default: true] If you don't want to create your own log in, sign up, forgot password and password reset pages from scratch you can use this generator. The pages will be diff --git a/packages/cli/src/commands/generate/dbAuth/__tests__/__snapshots__/dbAuth.test.js.snap b/packages/cli/src/commands/generate/dbAuth/__tests__/__snapshots__/dbAuth.test.js.snap new file mode 100644 index 000000000000..9046ed90f5ed --- /dev/null +++ b/packages/cli/src/commands/generate/dbAuth/__tests__/__snapshots__/dbAuth.test.js.snap @@ -0,0 +1,4561 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dbAuth handler exits when all files are skipped 1`] = ` +[ + " +No files to generate. +", +] +`; + +exports[`dbAuth handler produces the correct files with custom password set via flag 1`] = ` +"import { useEffect, useRef } from 'react' + +import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ForgotPasswordPage = () => { + const { isAuthenticated, forgotPassword } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const usernameRef = useRef(null) + useEffect(() => { + usernameRef?.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await forgotPassword(data.username) + + if (response.error) { + toast.error(response.error) + } else { + // The function \`forgotPassword.handler\` in api/src/functions/auth.js has + // been invoked, let the user know how to get the link to reset their + // password (sent in email, perhaps?) + toast.success('A link to reset your secret was sent to ' + response.email) + + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

Forgot Secret

+
+ +
+
+
+
+ + + + +
+ +
+ Submit +
+
+
+
+
+
+
+ + ) +} + +export default ForgotPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom password set via flag 2`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const LoginPage = () => { + const { isAuthenticated, logIn } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const usernameRef = useRef(null) + useEffect(() => { + usernameRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await logIn({ + username: data.username, + password: data.secret, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + toast.success('Welcome back!') + } + } + + return ( + <> + + +
+ +
+
+
+

Login

+
+ +
+
+
+ + + + + + + + +
+ + Forgot Secret? + +
+ + + +
+ Login +
+ +
+
+
+
+ Don't have an account?{' '} + + Sign up! + +
+
+
+ + ) +} + +export default LoginPage +" +`; + +exports[`dbAuth handler produces the correct files with custom password set via flag 3`] = ` +"import { useEffect, useRef, useState } from 'react' + +import { + Form, + Label, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ResetPasswordPage = ({ resetToken }) => { + const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } = + useAuth() + const [enabled, setEnabled] = useState(true) + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + useEffect(() => { + const validateToken = async () => { + const response = await validateResetToken(resetToken) + if (response.error) { + setEnabled(false) + toast.error(response.error) + } else { + setEnabled(true) + } + } + validateToken() + }, []) + + const secretRef = useRef(null) + useEffect(() => { + secretRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await resetPassword({ + resetToken, + password: data.secret, + }) + + if (response.error) { + toast.error(response.error) + } else { + toast.success('Secret changed!') + await reauthenticate() + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

Reset Secret

+
+ +
+
+
+
+ + + + +
+ +
+ + Submit + +
+
+
+
+
+
+
+ + ) +} + +export default ResetPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom password set via flag 4`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + FieldError, + Submit, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const SignupPage = () => { + const { isAuthenticated, signUp } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + // focus on username box on page load + const usernameRef = useRef(null) + useEffect(() => { + usernameRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await signUp({ + username: data.username, + password: data.secret, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + // user is signed in automatically + toast.success('Welcome!') + } + } + + return ( + <> + + +
+ +
+
+
+

Signup

+
+ +
+
+
+ + + + + + + + + + +
+ + Sign Up + +
+ +
+
+
+
+ Already have an account?{' '} + + Log in! + +
+
+
+ + ) +} + +export default SignupPage +" +`; + +exports[`dbAuth handler produces the correct files with custom password set via prompt 1`] = ` +"import { useEffect, useRef } from 'react' + +import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ForgotPasswordPage = () => { + const { isAuthenticated, forgotPassword } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const usernameRef = useRef(null) + useEffect(() => { + usernameRef?.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await forgotPassword(data.username) + + if (response.error) { + toast.error(response.error) + } else { + // The function \`forgotPassword.handler\` in api/src/functions/auth.js has + // been invoked, let the user know how to get the link to reset their + // password (sent in email, perhaps?) + toast.success('A link to reset your secret was sent to ' + response.email) + + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

Forgot Secret

+
+ +
+
+
+
+ + + + +
+ +
+ Submit +
+
+
+
+
+
+
+ + ) +} + +export default ForgotPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom password set via prompt 2`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const LoginPage = () => { + const { isAuthenticated, logIn } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const usernameRef = useRef(null) + useEffect(() => { + usernameRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await logIn({ + username: data.username, + password: data.secret, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + toast.success('Welcome back!') + } + } + + return ( + <> + + +
+ +
+
+
+

Login

+
+ +
+
+
+ + + + + + + + +
+ + Forgot Secret? + +
+ + + +
+ Login +
+ +
+
+
+
+ Don't have an account?{' '} + + Sign up! + +
+
+
+ + ) +} + +export default LoginPage +" +`; + +exports[`dbAuth handler produces the correct files with custom password set via prompt 3`] = ` +"import { useEffect, useRef, useState } from 'react' + +import { + Form, + Label, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ResetPasswordPage = ({ resetToken }) => { + const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } = + useAuth() + const [enabled, setEnabled] = useState(true) + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + useEffect(() => { + const validateToken = async () => { + const response = await validateResetToken(resetToken) + if (response.error) { + setEnabled(false) + toast.error(response.error) + } else { + setEnabled(true) + } + } + validateToken() + }, []) + + const secretRef = useRef(null) + useEffect(() => { + secretRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await resetPassword({ + resetToken, + password: data.secret, + }) + + if (response.error) { + toast.error(response.error) + } else { + toast.success('Secret changed!') + await reauthenticate() + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

Reset Secret

+
+ +
+
+
+
+ + + + +
+ +
+ + Submit + +
+
+
+
+
+
+
+ + ) +} + +export default ResetPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom password set via prompt 4`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + FieldError, + Submit, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const SignupPage = () => { + const { isAuthenticated, signUp } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + // focus on username box on page load + const usernameRef = useRef(null) + useEffect(() => { + usernameRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await signUp({ + username: data.username, + password: data.secret, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + // user is signed in automatically + toast.success('Welcome!') + } + } + + return ( + <> + + +
+ +
+
+
+

Signup

+
+ +
+
+
+ + + + + + + + + + +
+ + Sign Up + +
+ +
+
+
+
+ Already have an account?{' '} + + Log in! + +
+
+
+ + ) +} + +export default SignupPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via flag 1`] = ` +"import { useEffect, useRef } from 'react' + +import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ForgotPasswordPage = () => { + const { isAuthenticated, forgotPassword } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const emailRef = useRef(null) + useEffect(() => { + emailRef?.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await forgotPassword(data.email) + + if (response.error) { + toast.error(response.error) + } else { + // The function \`forgotPassword.handler\` in api/src/functions/auth.js has + // been invoked, let the user know how to get the link to reset their + // password (sent in email, perhaps?) + toast.success('A link to reset your secret was sent to ' + response.email) + + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

Forgot Secret

+
+ +
+
+
+
+ + + + +
+ +
+ Submit +
+
+
+
+
+
+
+ + ) +} + +export default ForgotPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via flag 2`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const LoginPage = () => { + const { isAuthenticated, logIn } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const emailRef = useRef(null) + useEffect(() => { + emailRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await logIn({ + username: data.email, + password: data.secret, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + toast.success('Welcome back!') + } + } + + return ( + <> + + +
+ +
+
+
+

Login

+
+ +
+
+
+ + + + + + + + +
+ + Forgot Secret? + +
+ + + +
+ Login +
+ +
+
+
+
+ Don't have an account?{' '} + + Sign up! + +
+
+
+ + ) +} + +export default LoginPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via flag 3`] = ` +"import { useEffect, useRef, useState } from 'react' + +import { + Form, + Label, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ResetPasswordPage = ({ resetToken }) => { + const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } = + useAuth() + const [enabled, setEnabled] = useState(true) + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + useEffect(() => { + const validateToken = async () => { + const response = await validateResetToken(resetToken) + if (response.error) { + setEnabled(false) + toast.error(response.error) + } else { + setEnabled(true) + } + } + validateToken() + }, []) + + const secretRef = useRef(null) + useEffect(() => { + secretRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await resetPassword({ + resetToken, + password: data.secret, + }) + + if (response.error) { + toast.error(response.error) + } else { + toast.success('Secret changed!') + await reauthenticate() + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

Reset Secret

+
+ +
+
+
+
+ + + + +
+ +
+ + Submit + +
+
+
+
+
+
+
+ + ) +} + +export default ResetPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via flag 4`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + FieldError, + Submit, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const SignupPage = () => { + const { isAuthenticated, signUp } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + // focus on email box on page load + const emailRef = useRef(null) + useEffect(() => { + emailRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await signUp({ + username: data.email, + password: data.secret, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + // user is signed in automatically + toast.success('Welcome!') + } + } + + return ( + <> + + +
+ +
+
+
+

Signup

+
+ +
+
+
+ + + + + + + + + + +
+ + Sign Up + +
+ +
+
+
+
+ Already have an account?{' '} + + Log in! + +
+
+
+ + ) +} + +export default SignupPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via prompt 1`] = ` +"import { useEffect, useRef } from 'react' + +import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ForgotPasswordPage = () => { + const { isAuthenticated, forgotPassword } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const emailRef = useRef(null) + useEffect(() => { + emailRef?.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await forgotPassword(data.email) + + if (response.error) { + toast.error(response.error) + } else { + // The function \`forgotPassword.handler\` in api/src/functions/auth.js has + // been invoked, let the user know how to get the link to reset their + // password (sent in email, perhaps?) + toast.success('A link to reset your secret was sent to ' + response.email) + + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

Forgot Secret

+
+ +
+
+
+
+ + + + +
+ +
+ Submit +
+
+
+
+
+
+
+ + ) +} + +export default ForgotPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via prompt 2`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const LoginPage = () => { + const { isAuthenticated, logIn } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const emailRef = useRef(null) + useEffect(() => { + emailRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await logIn({ + username: data.email, + password: data.secret, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + toast.success('Welcome back!') + } + } + + return ( + <> + + +
+ +
+
+
+

Login

+
+ +
+
+
+ + + + + + + + +
+ + Forgot Secret? + +
+ + + +
+ Login +
+ +
+
+
+
+ Don't have an account?{' '} + + Sign up! + +
+
+
+ + ) +} + +export default LoginPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via prompt 3`] = ` +"import { useEffect, useRef, useState } from 'react' + +import { + Form, + Label, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ResetPasswordPage = ({ resetToken }) => { + const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } = + useAuth() + const [enabled, setEnabled] = useState(true) + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + useEffect(() => { + const validateToken = async () => { + const response = await validateResetToken(resetToken) + if (response.error) { + setEnabled(false) + toast.error(response.error) + } else { + setEnabled(true) + } + } + validateToken() + }, []) + + const secretRef = useRef(null) + useEffect(() => { + secretRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await resetPassword({ + resetToken, + password: data.secret, + }) + + if (response.error) { + toast.error(response.error) + } else { + toast.success('Secret changed!') + await reauthenticate() + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

Reset Secret

+
+ +
+
+
+
+ + + + +
+ +
+ + Submit + +
+
+
+
+
+
+
+ + ) +} + +export default ResetPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via prompt 4`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + FieldError, + Submit, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const SignupPage = () => { + const { isAuthenticated, signUp } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + // focus on email box on page load + const emailRef = useRef(null) + useEffect(() => { + emailRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await signUp({ + username: data.email, + password: data.secret, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + // user is signed in automatically + toast.success('Welcome!') + } + } + + return ( + <> + + +
+ +
+
+
+

Signup

+
+ +
+
+
+ + + + + + + + + + +
+ + Sign Up + +
+ +
+
+
+
+ Already have an account?{' '} + + Log in! + +
+
+
+ + ) +} + +export default SignupPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via prompt and with webauthn enabled via flag 1`] = ` +"import { useEffect, useRef } from 'react' + +import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ForgotPasswordPage = () => { + const { isAuthenticated, forgotPassword } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const emailRef = useRef(null) + useEffect(() => { + emailRef?.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await forgotPassword(data.email) + + if (response.error) { + toast.error(response.error) + } else { + // The function \`forgotPassword.handler\` in api/src/functions/auth.js has + // been invoked, let the user know how to get the link to reset their + // password (sent in email, perhaps?) + toast.success('A link to reset your secret was sent to ' + response.email) + + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

Forgot Secret

+
+ +
+
+
+
+ + + + +
+ +
+ Submit +
+
+
+
+
+
+
+ + ) +} + +export default ForgotPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via prompt and with webauthn enabled via flag 2`] = ` +"import { useRef, useState } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const WELCOME_MESSAGE = 'Welcome back!' +const REDIRECT = routes.home() + +const LoginPage = ({ type }) => { + const { + isAuthenticated, + client: webAuthn, + loading, + logIn, + reauthenticate, + } = useAuth() + const [shouldShowWebAuthn, setShouldShowWebAuthn] = useState(false) + const [showWebAuthn, setShowWebAuthn] = useState( + webAuthn.isEnabled() && type !== 'password' + ) + + // should redirect right after login or wait to show the webAuthn prompts? + useEffect(() => { + if (isAuthenticated && (!shouldShowWebAuthn || webAuthn.isEnabled())) { + navigate(REDIRECT) + } + }, [isAuthenticated, shouldShowWebAuthn]) + + // if WebAuthn is enabled, show the prompt as soon as the page loads + useEffect(() => { + if (!loading && !isAuthenticated && showWebAuthn) { + onAuthenticate() + } + }, [loading, isAuthenticated]) + + // focus on the email field as soon as the page loads + const emailRef = useRef() + useEffect(() => { + emailRef.current && emailRef.current.focus() + }, []) + + const onSubmit = async (data) => { + const webAuthnSupported = await webAuthn.isSupported() + + if (webAuthnSupported) { + setShouldShowWebAuthn(true) + } + const response = await logIn({ + username: data.email, + password: data.secret, + }) + + if (response.message) { + // auth details good, but user not logged in + toast(response.message) + } else if (response.error) { + // error while authenticating + toast.error(response.error) + } else { + // user logged in + if (webAuthnSupported) { + setShowWebAuthn(true) + } else { + toast.success(WELCOME_MESSAGE) + } + } + } + + const onAuthenticate = async () => { + try { + await webAuthn.authenticate() + await reauthenticate() + toast.success(WELCOME_MESSAGE) + navigate(REDIRECT) + } catch (e) { + if (e.name === 'WebAuthnDeviceNotFoundError') { + toast.error('Device not found, log in with Email/Secret to continue') + + setShowWebAuthn(false) + } else { + toast.error(e.message) + } + } + } + + const onRegister = async () => { + try { + await webAuthn.register() + toast.success(WELCOME_MESSAGE) + navigate(REDIRECT) + } catch (e) { + toast.error(e.message) + } + } + + const onSkip = () => { + toast.success(WELCOME_MESSAGE) + setShouldShowWebAuthn(false) + } + + const AuthWebAuthnPrompt = () => { + return ( +
+

WebAuthn Login Enabled

+

Log in with your fingerprint, face or PIN

+
+ +
+
+ ) + } + + const RegisterWebAuthnPrompt = () => ( +
+

No more Secrets!

+

+ Depending on your device you can log in with your fingerprint, face or + PIN next time. +

+
+ + +
+
+ ) + + const PasswordForm = () => ( +
+ + + + + + + + +
+ + Forgot Secret? + +
+ + + +
+ Login +
+ + ) + + const formToRender = () => { + if (showWebAuthn) { + if (webAuthn.isEnabled()) { + return + } else { + return + } + } else { + return + } + } + + const linkToRender = () => { + if (showWebAuthn) { + if (webAuthn.isEnabled()) { + return ( +
+ or login with {' '} + + email and secret + +
+ ) + } + } else { + return ( +
+ Don't have an account?{' '} + + Sign up! + +
+ ) + } + } + + if (loading) { + return null + } + + return ( + <> + + +
+ +
+
+
+

Login

+
+ +
+
{formToRender()}
+
+
+ {linkToRender()} +
+
+ + ) +} + +export default LoginPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via prompt and with webauthn enabled via flag 3`] = ` +"import { useEffect, useRef, useState } from 'react' + +import { + Form, + Label, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ResetPasswordPage = ({ resetToken }) => { + const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } = + useAuth() + const [enabled, setEnabled] = useState(true) + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + useEffect(() => { + const validateToken = async () => { + const response = await validateResetToken(resetToken) + if (response.error) { + setEnabled(false) + toast.error(response.error) + } else { + setEnabled(true) + } + } + validateToken() + }, []) + + const secretRef = useRef(null) + useEffect(() => { + secretRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await resetPassword({ + resetToken, + password: data.secret, + }) + + if (response.error) { + toast.error(response.error) + } else { + toast.success('Secret changed!') + await reauthenticate() + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

Reset Secret

+
+ +
+
+
+
+ + + + +
+ +
+ + Submit + +
+
+
+
+
+
+
+ + ) +} + +export default ResetPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via prompt and with webauthn enabled via flag 4`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + FieldError, + Submit, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const SignupPage = () => { + const { isAuthenticated, signUp } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + // focus on email box on page load + const emailRef = useRef(null) + useEffect(() => { + emailRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await signUp({ + username: data.email, + password: data.secret, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + // user is signed in automatically + toast.success('Welcome!') + } + } + + return ( + <> + + +
+ +
+
+
+

Signup

+
+ +
+
+
+ + + + + + + + + + +
+ + Sign Up + +
+ +
+
+
+
+ Already have an account?{' '} + + Log in! + +
+
+
+ + ) +} + +export default SignupPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via prompt and with webauthn enabled via prompt 1`] = ` +"import { useEffect, useRef } from 'react' + +import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ForgotPasswordPage = () => { + const { isAuthenticated, forgotPassword } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const emailRef = useRef(null) + useEffect(() => { + emailRef?.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await forgotPassword(data.email) + + if (response.error) { + toast.error(response.error) + } else { + // The function \`forgotPassword.handler\` in api/src/functions/auth.js has + // been invoked, let the user know how to get the link to reset their + // password (sent in email, perhaps?) + toast.success('A link to reset your secret was sent to ' + response.email) + + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

Forgot Secret

+
+ +
+
+
+
+ + + + +
+ +
+ Submit +
+
+
+
+
+
+
+ + ) +} + +export default ForgotPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via prompt and with webauthn enabled via prompt 2`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const LoginPage = () => { + const { isAuthenticated, logIn } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const emailRef = useRef(null) + useEffect(() => { + emailRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await logIn({ + username: data.email, + password: data.secret, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + toast.success('Welcome back!') + } + } + + return ( + <> + + +
+ +
+
+
+

Login

+
+ +
+
+
+ + + + + + + + +
+ + Forgot Secret? + +
+ + + +
+ Login +
+ +
+
+
+
+ Don't have an account?{' '} + + Sign up! + +
+
+
+ + ) +} + +export default LoginPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via prompt and with webauthn enabled via prompt 3`] = ` +"import { useEffect, useRef, useState } from 'react' + +import { + Form, + Label, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ResetPasswordPage = ({ resetToken }) => { + const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } = + useAuth() + const [enabled, setEnabled] = useState(true) + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + useEffect(() => { + const validateToken = async () => { + const response = await validateResetToken(resetToken) + if (response.error) { + setEnabled(false) + toast.error(response.error) + } else { + setEnabled(true) + } + } + validateToken() + }, []) + + const secretRef = useRef(null) + useEffect(() => { + secretRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await resetPassword({ + resetToken, + password: data.secret, + }) + + if (response.error) { + toast.error(response.error) + } else { + toast.success('Secret changed!') + await reauthenticate() + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

Reset Secret

+
+ +
+
+
+
+ + + + +
+ +
+ + Submit + +
+
+
+
+
+
+
+ + ) +} + +export default ResetPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username and password set via prompt and with webauthn enabled via prompt 4`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + FieldError, + Submit, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const SignupPage = () => { + const { isAuthenticated, signUp } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + // focus on email box on page load + const emailRef = useRef(null) + useEffect(() => { + emailRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await signUp({ + username: data.email, + password: data.secret, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + // user is signed in automatically + toast.success('Welcome!') + } + } + + return ( + <> + + +
+ +
+
+
+

Signup

+
+ +
+
+
+ + + + + + + + + + +
+ + Sign Up + +
+ +
+
+
+
+ Already have an account?{' '} + + Log in! + +
+
+
+ + ) +} + +export default SignupPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username set via flag 1`] = ` +"import { useEffect, useRef } from 'react' + +import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ForgotPasswordPage = () => { + const { isAuthenticated, forgotPassword } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const emailRef = useRef(null) + useEffect(() => { + emailRef?.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await forgotPassword(data.email) + + if (response.error) { + toast.error(response.error) + } else { + // The function \`forgotPassword.handler\` in api/src/functions/auth.js has + // been invoked, let the user know how to get the link to reset their + // password (sent in email, perhaps?) + toast.success( + 'A link to reset your password was sent to ' + response.email + ) + + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

+ Forgot Password +

+
+ +
+
+
+
+ + + + +
+ +
+ Submit +
+
+
+
+
+
+
+ + ) +} + +export default ForgotPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username set via flag 2`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const LoginPage = () => { + const { isAuthenticated, logIn } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const emailRef = useRef(null) + useEffect(() => { + emailRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await logIn({ + username: data.email, + password: data.password, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + toast.success('Welcome back!') + } + } + + return ( + <> + + +
+ +
+
+
+

Login

+
+ +
+
+
+ + + + + + + + +
+ + Forgot Password? + +
+ + + +
+ Login +
+ +
+
+
+
+ Don't have an account?{' '} + + Sign up! + +
+
+
+ + ) +} + +export default LoginPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username set via flag 3`] = ` +"import { useEffect, useRef, useState } from 'react' + +import { + Form, + Label, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ResetPasswordPage = ({ resetToken }) => { + const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } = + useAuth() + const [enabled, setEnabled] = useState(true) + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + useEffect(() => { + const validateToken = async () => { + const response = await validateResetToken(resetToken) + if (response.error) { + setEnabled(false) + toast.error(response.error) + } else { + setEnabled(true) + } + } + validateToken() + }, []) + + const passwordRef = useRef(null) + useEffect(() => { + passwordRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await resetPassword({ + resetToken, + password: data.password, + }) + + if (response.error) { + toast.error(response.error) + } else { + toast.success('Password changed!') + await reauthenticate() + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

+ Reset Password +

+
+ +
+
+
+
+ + + + +
+ +
+ + Submit + +
+
+
+
+
+
+
+ + ) +} + +export default ResetPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username set via flag 4`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + FieldError, + Submit, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const SignupPage = () => { + const { isAuthenticated, signUp } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + // focus on email box on page load + const emailRef = useRef(null) + useEffect(() => { + emailRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await signUp({ + username: data.email, + password: data.password, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + // user is signed in automatically + toast.success('Welcome!') + } + } + + return ( + <> + + +
+ +
+
+
+

Signup

+
+ +
+
+
+ + + + + + + + + + +
+ + Sign Up + +
+ +
+
+
+
+ Already have an account?{' '} + + Log in! + +
+
+
+ + ) +} + +export default SignupPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username set via prompt 1`] = ` +"import { useEffect, useRef } from 'react' + +import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ForgotPasswordPage = () => { + const { isAuthenticated, forgotPassword } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const emailRef = useRef(null) + useEffect(() => { + emailRef?.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await forgotPassword(data.email) + + if (response.error) { + toast.error(response.error) + } else { + // The function \`forgotPassword.handler\` in api/src/functions/auth.js has + // been invoked, let the user know how to get the link to reset their + // password (sent in email, perhaps?) + toast.success( + 'A link to reset your password was sent to ' + response.email + ) + + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

+ Forgot Password +

+
+ +
+
+
+
+ + + + +
+ +
+ Submit +
+
+
+
+
+
+
+ + ) +} + +export default ForgotPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username set via prompt 2`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const LoginPage = () => { + const { isAuthenticated, logIn } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const emailRef = useRef(null) + useEffect(() => { + emailRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await logIn({ + username: data.email, + password: data.password, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + toast.success('Welcome back!') + } + } + + return ( + <> + + +
+ +
+
+
+

Login

+
+ +
+
+
+ + + + + + + + +
+ + Forgot Password? + +
+ + + +
+ Login +
+ +
+
+
+
+ Don't have an account?{' '} + + Sign up! + +
+
+
+ + ) +} + +export default LoginPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username set via prompt 3`] = ` +"import { useEffect, useRef, useState } from 'react' + +import { + Form, + Label, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ResetPasswordPage = ({ resetToken }) => { + const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } = + useAuth() + const [enabled, setEnabled] = useState(true) + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + useEffect(() => { + const validateToken = async () => { + const response = await validateResetToken(resetToken) + if (response.error) { + setEnabled(false) + toast.error(response.error) + } else { + setEnabled(true) + } + } + validateToken() + }, []) + + const passwordRef = useRef(null) + useEffect(() => { + passwordRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await resetPassword({ + resetToken, + password: data.password, + }) + + if (response.error) { + toast.error(response.error) + } else { + toast.success('Password changed!') + await reauthenticate() + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

+ Reset Password +

+
+ +
+
+
+
+ + + + +
+ +
+ + Submit + +
+
+
+
+
+
+
+ + ) +} + +export default ResetPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with custom username set via prompt 4`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + FieldError, + Submit, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const SignupPage = () => { + const { isAuthenticated, signUp } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + // focus on email box on page load + const emailRef = useRef(null) + useEffect(() => { + emailRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await signUp({ + username: data.email, + password: data.password, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + // user is signed in automatically + toast.success('Welcome!') + } + } + + return ( + <> + + +
+ +
+
+
+

Signup

+
+ +
+
+
+ + + + + + + + + + +
+ + Sign Up + +
+ +
+
+
+
+ Already have an account?{' '} + + Log in! + +
+
+
+ + ) +} + +export default SignupPage +" +`; + +exports[`dbAuth handler produces the correct files with default labels 1`] = ` +"import { useEffect, useRef } from 'react' + +import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ForgotPasswordPage = () => { + const { isAuthenticated, forgotPassword } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const usernameRef = useRef(null) + useEffect(() => { + usernameRef?.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await forgotPassword(data.username) + + if (response.error) { + toast.error(response.error) + } else { + // The function \`forgotPassword.handler\` in api/src/functions/auth.js has + // been invoked, let the user know how to get the link to reset their + // password (sent in email, perhaps?) + toast.success( + 'A link to reset your password was sent to ' + response.email + ) + + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

+ Forgot Password +

+
+ +
+
+
+
+ + + + +
+ +
+ Submit +
+
+
+
+
+
+
+ + ) +} + +export default ForgotPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with default labels 2`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const LoginPage = () => { + const { isAuthenticated, logIn } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + const usernameRef = useRef(null) + useEffect(() => { + usernameRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await logIn({ + username: data.username, + password: data.password, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + toast.success('Welcome back!') + } + } + + return ( + <> + + +
+ +
+
+
+

Login

+
+ +
+
+
+ + + + + + + + +
+ + Forgot Password? + +
+ + + +
+ Login +
+ +
+
+
+
+ Don't have an account?{' '} + + Sign up! + +
+
+
+ + ) +} + +export default LoginPage +" +`; + +exports[`dbAuth handler produces the correct files with default labels 3`] = ` +"import { useEffect, useRef, useState } from 'react' + +import { + Form, + Label, + PasswordField, + Submit, + FieldError, +} from '@redwoodjs/forms' +import { navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const ResetPasswordPage = ({ resetToken }) => { + const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } = + useAuth() + const [enabled, setEnabled] = useState(true) + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + useEffect(() => { + const validateToken = async () => { + const response = await validateResetToken(resetToken) + if (response.error) { + setEnabled(false) + toast.error(response.error) + } else { + setEnabled(true) + } + } + validateToken() + }, []) + + const passwordRef = useRef(null) + useEffect(() => { + passwordRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await resetPassword({ + resetToken, + password: data.password, + }) + + if (response.error) { + toast.error(response.error) + } else { + toast.success('Password changed!') + await reauthenticate() + navigate(routes.login()) + } + } + + return ( + <> + + +
+ +
+
+
+

+ Reset Password +

+
+ +
+
+
+
+ + + + +
+ +
+ + Submit + +
+
+
+
+
+
+
+ + ) +} + +export default ResetPasswordPage +" +`; + +exports[`dbAuth handler produces the correct files with default labels 4`] = ` +"import { useRef } from 'react' +import { useEffect } from 'react' + +import { + Form, + Label, + TextField, + PasswordField, + FieldError, + Submit, +} from '@redwoodjs/forms' +import { Link, navigate, routes } from '@redwoodjs/router' +import { MetaTags } from '@redwoodjs/web' +import { toast, Toaster } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' + +const SignupPage = () => { + const { isAuthenticated, signUp } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + navigate(routes.home()) + } + }, [isAuthenticated]) + + // focus on username box on page load + const usernameRef = useRef(null) + useEffect(() => { + usernameRef.current?.focus() + }, []) + + const onSubmit = async (data) => { + const response = await signUp({ + username: data.username, + password: data.password, + }) + + if (response.message) { + toast(response.message) + } else if (response.error) { + toast.error(response.error) + } else { + // user is signed in automatically + toast.success('Welcome!') + } + } + + return ( + <> + + +
+ +
+
+
+

Signup

+
+ +
+
+
+ + + + + + + + + + +
+ + Sign Up + +
+ +
+
+
+
+ Already have an account?{' '} + + Log in! + +
+
+
+ + ) +} + +export default SignupPage +" +`; diff --git a/packages/cli/src/commands/generate/dbAuth/__tests__/dbAuth.test.js b/packages/cli/src/commands/generate/dbAuth/__tests__/dbAuth.test.js index 69bdd2cc44d5..aaf0737b8508 100644 --- a/packages/cli/src/commands/generate/dbAuth/__tests__/dbAuth.test.js +++ b/packages/cli/src/commands/generate/dbAuth/__tests__/dbAuth.test.js @@ -1,12 +1,69 @@ global.__dirname = __dirname + +jest.mock('fs') + +import fs from 'fs' import path from 'path' // Load mocks import '../../../../lib/test' +const realfs = jest.requireActual('fs') +import Enquirer from 'enquirer' + +import { getPaths } from '../../../../lib' import * as dbAuth from '../dbAuth' +// Mock files needed for each test +const mockFiles = {} + +const dbAuthTemplateFiles = [ + 'forgotPassword.tsx.template', + 'login.tsx.template', + 'login.webAuthn.tsx.template', + 'resetPassword.tsx.template', + 'signup.tsx.template', +] +dbAuthTemplateFiles.forEach((templateFilename) => { + mockFiles[path.join(__dirname, `../templates/${templateFilename}`)] = realfs + .readFileSync(path.join(__dirname, `../templates/${templateFilename}`)) + .toString() +}) + +mockFiles[ + path.join(__dirname, `../../scaffold/templates/assets/scaffold.css.template`) +] = realfs + .readFileSync( + path.join( + __dirname, + `../../scaffold/templates/assets/scaffold.css.template` + ) + ) + .toString() + +mockFiles[getPaths().web.routes] = realfs + .readFileSync( + path.join( + __dirname, + `../../../../../../../__fixtures__/example-todo-main/web/src/Routes.js` + ) + ) + .toString() + +mockFiles[getPaths().web.app] = realfs + .readFileSync( + path.join( + __dirname, + `../../../../../../../__fixtures__/example-todo-main/web/src/App.js` + ) + ) + .toString() + describe('dbAuth', () => { + beforeEach(() => { + fs.__setMockFiles(mockFiles) + }) + it('creates a login page', () => { expect(dbAuth.files(true, false)).toHaveProperty([ path.normalize('/path/to/project/web/src/pages/LoginPage/LoginPage.js'), @@ -24,4 +81,603 @@ describe('dbAuth', () => { path.normalize('/path/to/project/web/src/scaffold.css'), ]) }) + + describe('handler', () => { + it('exits when all files are skipped', async () => { + const mockExit = jest.spyOn(process, 'exit').mockImplementation() + const mockConsoleInfo = jest.spyOn(console, 'info').mockImplementation() + + await dbAuth.handler({ + listr2: { rendererSilent: true }, + usernameLabel: 'email', + passwordLabel: 'password', + webauthn: false, + skipForgot: true, + skipLogin: true, + skipReset: true, + skipSignup: true, + }) + + expect(mockConsoleInfo.mock.calls[0]).toMatchSnapshot() + expect(mockExit).toHaveBeenCalledWith(0) + + mockExit.mockRestore() + mockConsoleInfo.mockRestore() + }) + + it('prompt for username label', async () => { + let correctPrompt = false + + const customEnquirer = new Enquirer({ show: false }) + customEnquirer.on('prompt', (prompt) => { + if (prompt.state.message.includes('username label')) { + correctPrompt = true + } + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + }) + expect(correctPrompt).toBe(true) + }) + + it('does not prompt for username label when flag is given', async () => { + let correctPrompt = false + + const customEnquirer = new Enquirer({ show: false }) + customEnquirer.on('prompt', (prompt) => { + if (prompt.state.message.includes('username label')) { + correctPrompt = true + } + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + usernameLabel: 'email', + }) + expect(correctPrompt).toBe(false) + }) + + it('prompt for password label', async () => { + let correctPrompt = false + + const customEnquirer = new Enquirer({ show: false }) + customEnquirer.on('prompt', (prompt) => { + if (prompt.state.message.includes('password label')) { + correctPrompt = true + } + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + }) + expect(correctPrompt).toBe(true) + }) + + it('does not prompt for password label when flag is given', async () => { + let correctPrompt = false + + const customEnquirer = new Enquirer({ show: false }) + customEnquirer.on('prompt', (prompt) => { + if (prompt.state.message.includes('password label')) { + correctPrompt = true + } + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + passwordLabel: 'secret', + }) + expect(correctPrompt).toBe(false) + }) + + it('prompt for webauthn', async () => { + let correctPrompt = false + + const customEnquirer = new Enquirer({ show: false }) + customEnquirer.on('prompt', (prompt) => { + if (prompt.state.message.includes('Enable WebAuthn')) { + correctPrompt = true + } + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + }) + expect(correctPrompt).toBe(true) + }) + + it('does not prompt for webauthn when flag is given', async () => { + let correctPrompt = false + + const customEnquirer = new Enquirer({ show: false }) + customEnquirer.on('prompt', (prompt) => { + if (prompt.state.message.includes('Enable WebAuthn')) { + correctPrompt = true + } + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + webauthn: false, + }) + expect(correctPrompt).toBe(false) + }) + + it('produces the correct files with default labels', async () => { + const customEnquirer = new Enquirer() + customEnquirer.on('prompt', (prompt) => { + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + }) + + const forgotPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js' + ) + ) + .toString() + expect(forgotPasswordPage).toMatchSnapshot() + + const loginPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/LoginPage/LoginPage.js' + ) + ) + .toString() + expect(loginPage).toMatchSnapshot() + + const resetPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ResetPasswordPage/ResetPasswordPage.js' + ) + ) + .toString() + expect(resetPasswordPage).toMatchSnapshot() + + const signupPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/SignupPage/SignupPage.js' + ) + ) + .toString() + expect(signupPage).toMatchSnapshot() + }) + + it('produces the correct files with custom username set via flag', async () => { + const customEnquirer = new Enquirer() + customEnquirer.on('prompt', (prompt) => { + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + usernameLabel: 'Email', + }) + + const forgotPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js' + ) + ) + .toString() + expect(forgotPasswordPage).toMatchSnapshot() + + const loginPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/LoginPage/LoginPage.js' + ) + ) + .toString() + expect(loginPage).toMatchSnapshot() + + const resetPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ResetPasswordPage/ResetPasswordPage.js' + ) + ) + .toString() + expect(resetPasswordPage).toMatchSnapshot() + + const signupPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/SignupPage/SignupPage.js' + ) + ) + .toString() + expect(signupPage).toMatchSnapshot() + }) + + it('produces the correct files with custom username set via prompt', async () => { + const customEnquirer = new Enquirer() + customEnquirer.on('prompt', (prompt) => { + if (prompt.state.message.includes('username label')) { + prompt.value = 'Email' + } + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + }) + + const forgotPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js' + ) + ) + .toString() + expect(forgotPasswordPage).toMatchSnapshot() + + const loginPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/LoginPage/LoginPage.js' + ) + ) + .toString() + expect(loginPage).toMatchSnapshot() + + const resetPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ResetPasswordPage/ResetPasswordPage.js' + ) + ) + .toString() + expect(resetPasswordPage).toMatchSnapshot() + + const signupPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/SignupPage/SignupPage.js' + ) + ) + .toString() + expect(signupPage).toMatchSnapshot() + }) + + it('produces the correct files with custom password set via flag', async () => { + const customEnquirer = new Enquirer() + customEnquirer.on('prompt', (prompt) => { + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + passwordLabel: 'Secret', + }) + + const forgotPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js' + ) + ) + .toString() + expect(forgotPasswordPage).toMatchSnapshot() + + const loginPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/LoginPage/LoginPage.js' + ) + ) + .toString() + expect(loginPage).toMatchSnapshot() + + const resetPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ResetPasswordPage/ResetPasswordPage.js' + ) + ) + .toString() + expect(resetPasswordPage).toMatchSnapshot() + + const signupPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/SignupPage/SignupPage.js' + ) + ) + .toString() + expect(signupPage).toMatchSnapshot() + }) + + it('produces the correct files with custom password set via prompt', async () => { + const customEnquirer = new Enquirer() + customEnquirer.on('prompt', (prompt) => { + if (prompt.state.message.includes('password label')) { + prompt.value = 'Secret' + } + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + }) + + const forgotPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js' + ) + ) + .toString() + expect(forgotPasswordPage).toMatchSnapshot() + + const loginPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/LoginPage/LoginPage.js' + ) + ) + .toString() + expect(loginPage).toMatchSnapshot() + + const resetPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ResetPasswordPage/ResetPasswordPage.js' + ) + ) + .toString() + expect(resetPasswordPage).toMatchSnapshot() + + const signupPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/SignupPage/SignupPage.js' + ) + ) + .toString() + expect(signupPage).toMatchSnapshot() + }) + + it('produces the correct files with custom username and password set via flag', async () => { + const customEnquirer = new Enquirer() + customEnquirer.on('prompt', (prompt) => { + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + usernameLabel: 'Email', + passwordLabel: 'Secret', + }) + + const forgotPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js' + ) + ) + .toString() + expect(forgotPasswordPage).toMatchSnapshot() + + const loginPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/LoginPage/LoginPage.js' + ) + ) + .toString() + expect(loginPage).toMatchSnapshot() + + const resetPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ResetPasswordPage/ResetPasswordPage.js' + ) + ) + .toString() + expect(resetPasswordPage).toMatchSnapshot() + + const signupPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/SignupPage/SignupPage.js' + ) + ) + .toString() + expect(signupPage).toMatchSnapshot() + }) + + it('produces the correct files with custom username and password set via prompt', async () => { + const customEnquirer = new Enquirer() + customEnquirer.on('prompt', (prompt) => { + if (prompt.state.message.includes('username label')) { + prompt.value = 'Email' + } + if (prompt.state.message.includes('password label')) { + prompt.value = 'Secret' + } + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + }) + + const forgotPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js' + ) + ) + .toString() + expect(forgotPasswordPage).toMatchSnapshot() + + const loginPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/LoginPage/LoginPage.js' + ) + ) + .toString() + expect(loginPage).toMatchSnapshot() + + const resetPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ResetPasswordPage/ResetPasswordPage.js' + ) + ) + .toString() + expect(resetPasswordPage).toMatchSnapshot() + + const signupPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/SignupPage/SignupPage.js' + ) + ) + .toString() + expect(signupPage).toMatchSnapshot() + }) + + it('produces the correct files with custom username and password set via prompt and with webauthn enabled via flag', async () => { + const customEnquirer = new Enquirer() + customEnquirer.on('prompt', (prompt) => { + if (prompt.state.message.includes('username label')) { + prompt.value = 'Email' + } + if (prompt.state.message.includes('password label')) { + prompt.value = 'Secret' + } + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + webauthn: true, + }) + + const forgotPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js' + ) + ) + .toString() + expect(forgotPasswordPage).toMatchSnapshot() + + const loginPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/LoginPage/LoginPage.js' + ) + ) + .toString() + expect(loginPage).toMatchSnapshot() + + const resetPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ResetPasswordPage/ResetPasswordPage.js' + ) + ) + .toString() + expect(resetPasswordPage).toMatchSnapshot() + + const signupPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/SignupPage/SignupPage.js' + ) + ) + .toString() + expect(signupPage).toMatchSnapshot() + }) + + it('produces the correct files with custom username and password set via prompt and with webauthn enabled via prompt', async () => { + const customEnquirer = new Enquirer() + customEnquirer.on('prompt', (prompt) => { + if (prompt.state.message.includes('username label')) { + prompt.value = 'Email' + } + if (prompt.state.message.includes('password label')) { + prompt.value = 'Secret' + } + if (prompt.state.message.includes('Enable WebAuthn')) { + prompt.value = true + } + prompt.submit() + }) + + await dbAuth.handler({ + enquirer: customEnquirer, + listr2: { rendererSilent: true }, + }) + + const forgotPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js' + ) + ) + .toString() + expect(forgotPasswordPage).toMatchSnapshot() + + const loginPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/LoginPage/LoginPage.js' + ) + ) + .toString() + expect(loginPage).toMatchSnapshot() + + const resetPasswordPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/ResetPasswordPage/ResetPasswordPage.js' + ) + ) + .toString() + expect(resetPasswordPage).toMatchSnapshot() + + const signupPage = fs + .readFileSync( + path.normalize( + '/path/to/project/web/src/pages/SignupPage/SignupPage.js' + ) + ) + .toString() + expect(signupPage).toMatchSnapshot() + }) + }) }) diff --git a/packages/cli/src/commands/generate/dbAuth/dbAuth.js b/packages/cli/src/commands/generate/dbAuth/dbAuth.js index df475af80f58..8703333aa6a6 100644 --- a/packages/cli/src/commands/generate/dbAuth/dbAuth.js +++ b/packages/cli/src/commands/generate/dbAuth/dbAuth.js @@ -1,9 +1,11 @@ import fs from 'fs' import path from 'path' +import { camelCase } from 'camel-case' +import Enquirer from 'enquirer' import { Listr } from 'listr2' -import prompts from 'prompts' import terminalLink from 'terminal-link' +import { titleCase } from 'title-case' import { addRoutesToRouterTask, @@ -93,12 +95,21 @@ export const builder = (yargs) => { description: 'Include WebAuthn support (TouchID/FaceID)', type: 'boolean', }) + .option('username-label', { + default: null, + description: 'Override default form label for username field', + type: 'string', + }) + .option('password-label', { + default: null, + description: 'Override default form label for password field', + type: 'string', + }) .option('rollback', { description: 'Revert all generator actions if an error occurs', type: 'boolean', default: true, }) - .epilogue( `Also see the ${terminalLink( 'Redwood CLI Reference', @@ -119,10 +130,24 @@ export const files = ({ skipLogin, skipReset, skipSignup, - webAuthn, + webauthn, + usernameLabel, + passwordLabel, }) => { const files = [] + usernameLabel = usernameLabel || 'username' + passwordLabel = passwordLabel || 'password' + + const templateVars = { + usernameLowerCase: usernameLabel.toLowerCase(), + usernameCamelCase: camelCase(usernameLabel), + usernameTitleCase: titleCase(usernameLabel), + passwordLowerCase: passwordLabel.toLowerCase(), + passwordCamelCase: camelCase(passwordLabel), + passwordTitleCase: titleCase(passwordLabel), + } + if (!skipForgot) { files.push( templateForComponentFile({ @@ -132,6 +157,7 @@ export const files = ({ webPathSection: 'pages', generator: 'dbAuth', templatePath: 'forgotPassword.tsx.template', + templateVars, }) ) } @@ -144,9 +170,10 @@ export const files = ({ extension: typescript ? '.tsx' : '.js', webPathSection: 'pages', generator: 'dbAuth', - templatePath: webAuthn + templatePath: webauthn ? 'login.webAuthn.tsx.template' : 'login.tsx.template', + templateVars, }) ) } @@ -160,6 +187,7 @@ export const files = ({ webPathSection: 'pages', generator: 'dbAuth', templatePath: 'resetPassword.tsx.template', + templateVars, }) ) } @@ -173,6 +201,7 @@ export const files = ({ webPathSection: 'pages', generator: 'dbAuth', templatePath: 'signup.tsx.template', + templateVars, }) ) } @@ -211,6 +240,8 @@ export const files = ({ } const tasks = ({ + enquirer, + listr2, force, tests, typescript, @@ -218,10 +249,81 @@ const tasks = ({ skipLogin, skipReset, skipSignup, - webAuthn, + webauthn, + usernameLabel, + passwordLabel, }) => { return new Listr( [ + { + title: 'Determining UI labels...', + skip: () => { + return usernameLabel && passwordLabel + }, + task: async (ctx, task) => { + return task.newListr([ + { + title: 'Username label', + task: async (subctx, subtask) => { + if (usernameLabel) { + subtask.skip( + `Argument username-label is set, using: "${usernameLabel}"` + ) + return + } + usernameLabel = await subtask.prompt({ + type: 'input', + name: 'username', + message: 'What would you like the username label to be:', + default: 'Username', + }) + subtask.title = `Username label: "${usernameLabel}"` + }, + }, + { + title: 'Password label', + task: async (subctx, subtask) => { + if (passwordLabel) { + subtask.skip( + `Argument password-label passed, using: "${passwordLabel}"` + ) + return + } + passwordLabel = await subtask.prompt({ + type: 'input', + name: 'password', + message: 'What would you like the password label to be:', + default: 'Password', + }) + subtask.title = `Password label: "${passwordLabel}"` + }, + }, + ]) + }, + }, + { + title: 'Querying WebAuthn addition...', + task: async (ctx, task) => { + if (webauthn != null) { + task.skip( + `Querying WebAuthn addition: argument webauthn passed, WebAuthn ${ + webauthn ? '' : 'not' + } included` + ) + return + } + const response = await task.prompt({ + type: 'confirm', + name: 'answer', + message: `Enable WebAuthn support (TouchID/FaceID) on LoginPage? See https://redwoodjs.com/docs/auth/dbAuth#webAuthn`, + default: false, + }) + webauthn = response + task.title = `Querying WebAuthn addition: WebAuthn addition ${ + webauthn ? '' : 'not' + } included` + }, + }, { title: 'Creating pages...', task: async () => { @@ -233,7 +335,9 @@ const tasks = ({ skipLogin, skipReset, skipSignup, - webAuthn, + webauthn, + usernameLabel, + passwordLabel, }), { overwriteExisting: force, @@ -254,28 +358,21 @@ const tasks = ({ { title: 'One more thing...', task: (ctx, task) => { - task.title = webAuthn ? WEBAUTHN_POST_INSTALL : POST_INSTALL + task.title = webauthn ? WEBAUTHN_POST_INSTALL : POST_INSTALL }, }, ], - { rendererOptions: { collapse: false }, exitOnError: true } + { + rendererSilent: () => listr2?.rendererSilent, + rendererOptions: { collapse: false }, + injectWrapper: { enquirer: enquirer || new Enquirer() }, + exitOnError: true, + } ) } export const handler = async (yargs) => { - let includeWebAuthn = yargs.webauthn - - if (includeWebAuthn === null) { - const response = await prompts({ - type: 'confirm', - name: 'answer', - message: `Enable WebAuthn support (TouchID/FaceID) on LoginPage? See https://redwoodjs.com/docs/auth/dbAuth#webAuthn`, - initial: false, - }) - includeWebAuthn = response.answer - } - - const t = tasks({ ...yargs, webAuthn: includeWebAuthn }) + const t = tasks({ ...yargs }) try { if (yargs.rollback) { diff --git a/packages/cli/src/commands/generate/dbAuth/templates/forgotPassword.tsx.template b/packages/cli/src/commands/generate/dbAuth/templates/forgotPassword.tsx.template index 8750916e6a8e..cf72f6efcb1e 100644 --- a/packages/cli/src/commands/generate/dbAuth/templates/forgotPassword.tsx.template +++ b/packages/cli/src/commands/generate/dbAuth/templates/forgotPassword.tsx.template @@ -16,13 +16,13 @@ const ForgotPasswordPage = () => { } }, [isAuthenticated]) - const usernameRef = useRef(null) + const ${usernameCamelCase}Ref = useRef(null) useEffect(() => { - usernameRef?.current?.focus() + ${usernameCamelCase}Ref?.current?.focus() }, []) - const onSubmit = async (data: { username: string }) => { - const response = await forgotPassword(data.username) + const onSubmit = async (data: { ${usernameCamelCase}: string }) => { + const response = await forgotPassword(data.${usernameCamelCase}) if (response.error) { toast.error(response.error) @@ -31,7 +31,7 @@ const ForgotPasswordPage = () => { // been invoked, let the user know how to get the link to reset their // password (sent in email, perhaps?) toast.success( - 'A link to reset your password was sent to ' + response.email + 'A link to reset your ${passwordLowerCase} was sent to ' + response.email ) navigate(routes.login()) } @@ -39,7 +39,7 @@ const ForgotPasswordPage = () => { return ( <> - +
@@ -47,7 +47,7 @@ const ForgotPasswordPage = () => {

- Forgot Password + Forgot ${passwordTitleCase}

@@ -56,23 +56,26 @@ const ForgotPasswordPage = () => {
- +
diff --git a/packages/cli/src/commands/generate/dbAuth/templates/login.tsx.template b/packages/cli/src/commands/generate/dbAuth/templates/login.tsx.template index cecb3ac40656..b09688c201e5 100644 --- a/packages/cli/src/commands/generate/dbAuth/templates/login.tsx.template +++ b/packages/cli/src/commands/generate/dbAuth/templates/login.tsx.template @@ -24,13 +24,13 @@ const LoginPage = () => { } }, [isAuthenticated]) - const usernameRef = useRef(null) + const ${usernameCamelCase}Ref = useRef(null) useEffect(() => { - usernameRef.current?.focus() + ${usernameCamelCase}Ref.current?.focus() }, []) const onSubmit = async (data: Record) => { - const response = await logIn({ ...data }) + const response = await logIn({ username: data.${usernameCamelCase}, password: data.${passwordCamelCase} }) if (response.message) { toast(response.message) @@ -57,43 +57,43 @@ const LoginPage = () => {
- + @@ -103,11 +103,11 @@ const LoginPage = () => { to={routes.forgotPassword()} className="rw-forgot-link" > - Forgot Password? + Forgot ${passwordTitleCase}?
- +
Login diff --git a/packages/cli/src/commands/generate/dbAuth/templates/login.webAuthn.tsx.template b/packages/cli/src/commands/generate/dbAuth/templates/login.webAuthn.tsx.template index 336af440e406..eadd3d4222c9 100644 --- a/packages/cli/src/commands/generate/dbAuth/templates/login.webAuthn.tsx.template +++ b/packages/cli/src/commands/generate/dbAuth/templates/login.webAuthn.tsx.template @@ -45,10 +45,10 @@ const LoginPage = ({ type }) => { } }, [loading, isAuthenticated]) - // focus on the username field as soon as the page loads - const usernameRef = useRef() + // focus on the ${usernameLowerCase} field as soon as the page loads + const ${usernameCamelCase}Ref = useRef() useEffect(() => { - usernameRef.current && usernameRef.current.focus() + ${usernameCamelCase}Ref.current && ${usernameCamelCase}Ref.current.focus() }, []) const onSubmit = async (data) => { @@ -57,7 +57,7 @@ const LoginPage = ({ type }) => { if (webAuthnSupported) { setShouldShowWebAuthn(true) } - const response = await logIn({ ...data }) + const response = await logIn({ username: data.${usernameCamelCase}, password: data.${passwordCamelCase} }) if (response.message) { // auth details good, but user not logged in @@ -84,7 +84,7 @@ const LoginPage = ({ type }) => { } catch (e) { if (e.name === 'WebAuthnDeviceNotFoundError') { toast.error( - 'Device not found, log in with username/password to continue' + 'Device not found, log in with ${usernameTitleCase}/${passwordTitleCase} to continue' ) setShowWebAuthn(false) } else { @@ -124,7 +124,7 @@ const LoginPage = ({ type }) => { const RegisterWebAuthnPrompt = () => (
-

No more passwords!

+

No more ${passwordTitleCase}s!

Depending on your device you can log in with your fingerprint, face or PIN next time. @@ -143,55 +143,55 @@ const LoginPage = ({ type }) => { const PasswordForm = () => ( - +

- Forgot Password? + Forgot ${passwordTitleCase}?
- +
Login @@ -218,7 +218,7 @@ const LoginPage = ({ type }) => { ) diff --git a/packages/cli/src/commands/generate/dbAuth/templates/resetPassword.tsx.template b/packages/cli/src/commands/generate/dbAuth/templates/resetPassword.tsx.template index 31744940b706..d42750aa615b 100644 --- a/packages/cli/src/commands/generate/dbAuth/templates/resetPassword.tsx.template +++ b/packages/cli/src/commands/generate/dbAuth/templates/resetPassword.tsx.template @@ -37,21 +37,21 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => { validateToken() }, []) - const passwordRef = useRef(null) + const ${passwordCamelCase}Ref = useRef(null) useEffect(() => { - passwordRef.current?.focus() + ${passwordCamelCase}Ref.current?.focus() }, []) const onSubmit = async (data: Record) => { const response = await resetPassword({ resetToken, - password: data.password, + password: data.${passwordCamelCase}, }) if (response.error) { toast.error(response.error) } else { - toast.success('Password changed!') + toast.success('${passwordTitleCase} changed!') await reauthenticate() navigate(routes.login()) } @@ -59,7 +59,7 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => { return ( <> - +
@@ -67,7 +67,7 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {

- Reset Password + Reset ${passwordTitleCase}

@@ -76,28 +76,28 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
- +
diff --git a/packages/cli/src/commands/generate/dbAuth/templates/signup.tsx.template b/packages/cli/src/commands/generate/dbAuth/templates/signup.tsx.template index f1974cdedc34..e84e61ce0012 100644 --- a/packages/cli/src/commands/generate/dbAuth/templates/signup.tsx.template +++ b/packages/cli/src/commands/generate/dbAuth/templates/signup.tsx.template @@ -24,14 +24,14 @@ const SignupPage = () => { } }, [isAuthenticated]) - // focus on email box on page load - const usernameRef = useRef(null) + // focus on ${usernameLowerCase} box on page load + const ${usernameCamelCase}Ref = useRef(null) useEffect(() => { - usernameRef.current?.focus() + ${usernameCamelCase}Ref.current?.focus() }, []) const onSubmit = async (data: Record) => { - const response = await signUp({ ...data }) + const response = await signUp({ username: data.${usernameCamelCase}, password: data.${passwordCamelCase} }) if (response.message) { toast(response.message) @@ -59,46 +59,46 @@ const SignupPage = () => {
- + - +
diff --git a/packages/cli/src/lib/test.js b/packages/cli/src/lib/test.js index 0495f54229ff..b2f46e5f1a3a 100644 --- a/packages/cli/src/lib/test.js +++ b/packages/cli/src/lib/test.js @@ -46,6 +46,7 @@ jest.mock('@redwoodjs/internal/dist/paths', () => { components: path.join(BASE_PATH, '/web/src/components'), layouts: path.join(BASE_PATH, '/web/src/layouts'), pages: path.join(BASE_PATH, '/web/src/pages'), + app: path.join(BASE_PATH, '/web/src/App.js'), }, scripts: path.join(BASE_PATH, 'scripts'), generated: { diff --git a/tasks/test-project/tasks.js b/tasks/test-project/tasks.js index edc1cd65c8bf..cee0e69962d5 100644 --- a/tasks/test-project/tasks.js +++ b/tasks/test-project/tasks.js @@ -449,7 +449,11 @@ async function apiTasks(outputPath, { verbose, linkWithLatestFwBuild }) { await execa('yarn rwfw project:copy', [], execaOptions) } - await execa('yarn rw g dbAuth --no-webauthn', [], execaOptions) + await execa( + 'yarn rw g dbAuth --no-webauthn --username-label=username --password-label=password', + [], + execaOptions + ) // update directive in contacts.sdl.ts const pathContactsSdl = `${OUTPUT_PATH}/api/src/graphql/contacts.sdl.ts`