diff --git a/api/db/migrations/20231027135109_create_part_schema/migration.sql b/api/db/migrations/20231031125027_user_and_part/migration.sql
similarity index 50%
rename from api/db/migrations/20231027135109_create_part_schema/migration.sql
rename to api/db/migrations/20231031125027_user_and_part/migration.sql
index d96ec31..8a2a681 100644
--- a/api/db/migrations/20231027135109_create_part_schema/migration.sql
+++ b/api/db/migrations/20231031125027_user_and_part/migration.sql
@@ -7,3 +7,17 @@ CREATE TABLE "Part" (
"imageUrl" TEXT NOT NULL DEFAULT '/no_image.png',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
+
+-- CreateTable
+CREATE TABLE "User" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "name" TEXT,
+ "email" TEXT NOT NULL,
+ "hashedPassword" TEXT NOT NULL,
+ "salt" TEXT NOT NULL,
+ "resetToken" TEXT,
+ "resetTokenExpiresAt" DATETIME
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
diff --git a/api/db/migrations/20231031125421_user_role/migration.sql b/api/db/migrations/20231031125421_user_role/migration.sql
new file mode 100644
index 0000000..676758b
--- /dev/null
+++ b/api/db/migrations/20231031125421_user_role/migration.sql
@@ -0,0 +1,12 @@
+-- CreateTable
+CREATE TABLE "UserRole" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "name" TEXT NOT NULL,
+ "userId" INTEGER,
+ CONSTRAINT "UserRole_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UserRole_name_userId_key" ON "UserRole"("name", "userId");
diff --git a/api/db/migrations/20231031145219_separate_name/migration.sql b/api/db/migrations/20231031145219_separate_name/migration.sql
new file mode 100644
index 0000000..4326da6
--- /dev/null
+++ b/api/db/migrations/20231031145219_separate_name/migration.sql
@@ -0,0 +1,26 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `name` on the `User` table. All the data in the column will be lost.
+ - Added the required column `firstName` to the `User` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `lastName` to the `User` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- RedefineTables
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_User" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "firstName" TEXT NOT NULL,
+ "lastName" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "hashedPassword" TEXT NOT NULL,
+ "salt" TEXT NOT NULL,
+ "resetToken" TEXT,
+ "resetTokenExpiresAt" DATETIME
+);
+INSERT INTO "new_User" ("email", "hashedPassword", "id", "resetToken", "resetTokenExpiresAt", "salt") SELECT "email", "hashedPassword", "id", "resetToken", "resetTokenExpiresAt", "salt" FROM "User";
+DROP TABLE "User";
+ALTER TABLE "new_User" RENAME TO "User";
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
+PRAGMA foreign_key_check;
+PRAGMA foreign_keys=ON;
diff --git a/api/db/schema.prisma b/api/db/schema.prisma
index d249aae..3c226f5 100644
--- a/api/db/schema.prisma
+++ b/api/db/schema.prisma
@@ -16,3 +16,26 @@ model Part {
imageUrl String @default("/no_image.png")
createdAt DateTime @default(now())
}
+
+model User {
+ id Int @id @default(autoincrement())
+ firstName String
+ lastName String
+ email String @unique
+ hashedPassword String
+ salt String
+ resetToken String?
+ resetTokenExpiresAt DateTime?
+ userRoles UserRole[]
+}
+
+model UserRole {
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now())
+ name String
+ user User? @relation(fields: [userId], references: [id])
+ userId Int?
+
+ @@unique([name, userId])
+}
diff --git a/api/package.json b/api/package.json
index 6bc4a45..d5b2642 100644
--- a/api/package.json
+++ b/api/package.json
@@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@redwoodjs/api": "6.3.2",
+ "@redwoodjs/auth-dbauth-api": "6.3.2",
"@redwoodjs/graphql-server": "6.3.2",
"filestack-js": "^3.27.0"
}
diff --git a/api/src/functions/auth.ts b/api/src/functions/auth.ts
new file mode 100644
index 0000000..edf0041
--- /dev/null
+++ b/api/src/functions/auth.ts
@@ -0,0 +1,185 @@
+import type { APIGatewayProxyEvent, Context } from 'aws-lambda'
+
+import {
+ DbAuthHandler,
+ DbAuthHandlerOptions,
+ PasswordValidationError,
+} from '@redwoodjs/auth-dbauth-api'
+
+import { db } from 'src/lib/db'
+
+export const handler = async (
+ event: APIGatewayProxyEvent,
+ context: Context
+) => {
+ const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = {
+ // handler() is invoked after verifying that a user was found with the given
+ // username. This is where you can send the user an email with a link to
+ // reset their password. With the default dbAuth routes and field names, the
+ // URL to reset the password will be:
+ //
+ // https://example.com/reset-password?resetToken=${user.resetToken}
+ //
+ // Whatever is returned from this function will be returned from
+ // the `forgotPassword()` function that is destructured from `useAuth()`
+ // You could use this return value to, for example, show the email
+ // address in a toast message so the user will know it worked and where
+ // to look for the email.
+ handler: (user) => {
+ // TODO: Forgot password link
+ return user
+ },
+
+ // How long the resetToken is valid for, in seconds (default is 24 hours)
+ expires: 60 * 60 * 24,
+
+ errors: {
+ // for security reasons you may want to be vague here rather than expose
+ // the fact that the email address wasn't found (prevents fishing for
+ // valid email addresses)
+ usernameNotFound: 'Email not found',
+ // if the user somehow gets around client validation
+ usernameRequired: 'Email is required',
+ },
+ }
+
+ const loginOptions: DbAuthHandlerOptions['login'] = {
+ // handler() is called after finding the user that matches the
+ // username/password provided at login, but before actually considering them
+ // logged in. The `user` argument will be the user in the database that
+ // matched the username/password.
+ //
+ // If you want to allow this user to log in simply return the user.
+ //
+ // If you want to prevent someone logging in for another reason (maybe they
+ // didn't validate their email yet), throw an error and it will be returned
+ // by the `logIn()` function from `useAuth()` in the form of:
+ // `{ message: 'Error message' }`
+ handler: (user) => {
+ return user
+ },
+
+ errors: {
+ usernameOrPasswordMissing: 'Both email and password are required',
+ usernameNotFound: 'Email ${username} not found',
+ // For security reasons you may want to make this the same as the
+ // usernameNotFound error so that a malicious user can't use the error
+ // to narrow down if it's the username or password that's incorrect
+ incorrectPassword: 'Incorrect password for ${username}',
+ },
+
+ // How long a user will remain logged in, in seconds
+ expires: 60 * 60 * 24 * 365 * 10,
+ }
+
+ const resetPasswordOptions: DbAuthHandlerOptions['resetPassword'] = {
+ // handler() is invoked after the password has been successfully updated in
+ // the database. Returning anything truthy will automatically log the user
+ // in. Return `false` otherwise, and in the Reset Password page redirect the
+ // user to the login page.
+ handler: (_user) => {
+ return true
+ },
+
+ // If `false` then the new password MUST be different from the current one
+ allowReusedPassword: false,
+
+ errors: {
+ // the resetToken is valid, but expired
+ resetTokenExpired: 'resetToken is expired',
+ // no user was found with the given resetToken
+ resetTokenInvalid: 'resetToken is invalid',
+ // the resetToken was not present in the URL
+ resetTokenRequired: 'resetToken is required',
+ // new password is the same as the old password (apparently they did not forget it)
+ reusedPassword: 'Must choose a new password',
+ },
+ }
+
+ const signupOptions: DbAuthHandlerOptions['signup'] = {
+ // Whatever you want to happen to your data on new user signup. Redwood will
+ // check for duplicate usernames before calling this handler. At a minimum
+ // you need to save the `username`, `hashedPassword` and `salt` to your
+ // user table. `userAttributes` contains any additional object members that
+ // were included in the object given to the `signUp()` function you got
+ // from `useAuth()`.
+ //
+ // If you want the user to be immediately logged in, return the user that
+ // was created.
+ //
+ // If this handler throws an error, it will be returned by the `signUp()`
+ // function in the form of: `{ error: 'Error message' }`.
+ //
+ // If this returns anything else, it will be returned by the
+ // `signUp()` function in the form of: `{ message: 'String here' }`.
+ handler: ({ username, hashedPassword, salt, userAttributes }) => {
+ return db.user.create({
+ data: {
+ email: username,
+ hashedPassword: hashedPassword,
+ salt: salt,
+ firstName: userAttributes.firstName,
+ lastName: userAttributes.lastName,
+ },
+ })
+ },
+
+ // Include any format checks for password here. Return `true` if the
+ // password is valid, otherwise throw a `PasswordValidationError`.
+ // Import the error along with `DbAuthHandler` from `@redwoodjs/api` above.
+ passwordValidation: (password) => {
+ if (password.length < 6)
+ throw new PasswordValidationError(
+ 'Password must be at least 6 characters'
+ )
+ else return true
+ },
+
+ errors: {
+ // `field` will be either "username" or "password"
+ fieldMissing: '${field} is required',
+ usernameTaken: 'Email `${username}` already in use',
+ },
+ }
+
+ const authHandler = new DbAuthHandler(event, context, {
+ // Provide prisma db client
+ db: db,
+
+ // The name of the property you'd call on `db` to access your user table.
+ // i.e. if your Prisma model is named `User` this value would be `user`, as in `db.user`
+ authModelAccessor: 'user',
+
+ // A map of what dbAuth calls a field to what your database calls it.
+ // `id` is whatever column you use to uniquely identify a user (probably
+ // something like `id` or `userId` or even `email`)
+ authFields: {
+ id: 'id',
+ username: 'email',
+ hashedPassword: 'hashedPassword',
+ salt: 'salt',
+ resetToken: 'resetToken',
+ resetTokenExpiresAt: 'resetTokenExpiresAt',
+ },
+
+ // Specifies attributes on the cookie that dbAuth sets in order to remember
+ // who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
+ cookie: {
+ HttpOnly: true,
+ Path: '/',
+ SameSite: 'Strict',
+ Secure: process.env.NODE_ENV !== 'development',
+
+ // If you need to allow other domains (besides the api side) access to
+ // the dbAuth session cookie:
+ // Domain: 'example.com',
+ },
+
+ forgotPassword: forgotPasswordOptions,
+ login: loginOptions,
+ resetPassword: resetPasswordOptions,
+ signup: signupOptions,
+ })
+
+ return await authHandler.invoke()
+}
diff --git a/api/src/functions/graphql.ts b/api/src/functions/graphql.ts
index f395c3b..5d8db6a 100644
--- a/api/src/functions/graphql.ts
+++ b/api/src/functions/graphql.ts
@@ -1,13 +1,17 @@
+import { authDecoder } from '@redwoodjs/auth-dbauth-api'
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
+import { getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
export const handler = createGraphQLHandler({
+ authDecoder,
+ getCurrentUser,
loggerConfig: { logger, options: {} },
directives,
sdls,
diff --git a/api/src/graphql/parts.sdl.ts b/api/src/graphql/parts.sdl.ts
index 622f814..7395a1e 100644
--- a/api/src/graphql/parts.sdl.ts
+++ b/api/src/graphql/parts.sdl.ts
@@ -9,8 +9,8 @@ export const schema = gql`
}
type Query {
- parts: [Part!]! @requireAuth
- part(id: Int!): Part @requireAuth
+ parts: [Part!]! @skipAuth
+ part(id: Int!): Part @skipAuth
}
input CreatePartInput {
diff --git a/api/src/lib/auth.ts b/api/src/lib/auth.ts
index f98fe93..bd808d0 100644
--- a/api/src/lib/auth.ts
+++ b/api/src/lib/auth.ts
@@ -1,25 +1,112 @@
+import type { Decoded } from '@redwoodjs/api'
+import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
+
+import { db } from './db'
+
/**
- * Once you are ready to add authentication to your application
- * you'll build out requireAuth() with real functionality. For
- * now we just return `true` so that the calls in services
- * have something to check against, simulating a logged
- * in user that is allowed to access that service.
+ * The session object sent in as the first argument to getCurrentUser() will
+ * have a single key `id` containing the unique ID of the logged in user
+ * (whatever field you set as `authFields.id` in your auth function config).
+ * You'll need to update the call to `db` below if you use a different model
+ * name or unique field name, for example:
*
- * See https://redwoodjs.com/docs/authentication for more info.
+ * return await db.profile.findUnique({ where: { email: session.id } })
+ * ───┬─── ──┬──
+ * model accessor ─┘ unique id field name ─┘
+ *
+ * !! BEWARE !! Anything returned from this function will be available to the
+ * client--it becomes the content of `currentUser` on the web side (as well as
+ * `context.currentUser` on the api side). You should carefully add additional
+ * fields to the `select` object below once you've decided they are safe to be
+ * seen if someone were to open the Web Inspector in their browser.
*/
-export const isAuthenticated = () => {
- return true
+export const getCurrentUser = async (session: Decoded) => {
+ if (!session || typeof session.id !== 'number') {
+ throw new Error('Invalid session')
+ }
+
+ return await db.user.findUnique({
+ where: { id: session.id },
+ select: { id: true, firstName: true },
+ })
}
-export const hasRole = ({ roles }) => {
- return roles !== undefined
+/**
+ * The user is authenticated if there is a currentUser in the context
+ *
+ * @returns {boolean} - If the currentUser is authenticated
+ */
+export const isAuthenticated = (): boolean => {
+ return !!context.currentUser
}
-// This is used by the redwood directive
-// in ./api/src/directives/requireAuth
+/**
+ * When checking role membership, roles can be a single value, a list, or none.
+ * You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client`
+ */
+type AllowedRoles = string | string[] | undefined
-// Roles are passed in by the requireAuth directive if you have auth setup
-// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
-export const requireAuth = ({ roles }) => {
- return isAuthenticated()
+/**
+ * Checks if the currentUser is authenticated (and assigned one of the given roles)
+ *
+ * @param roles: {@link AllowedRoles} - Checks if the currentUser is assigned one of these roles
+ *
+ * @returns {boolean} - Returns true if the currentUser is logged in and assigned one of the given roles,
+ * or when no roles are provided to check against. Otherwise returns false.
+ */
+export const hasRole = (roles: AllowedRoles): boolean => {
+ if (!isAuthenticated()) {
+ return false
+ }
+
+ const currentUserRoles = context.currentUser?.roles
+
+ if (typeof roles === 'string') {
+ if (typeof currentUserRoles === 'string') {
+ // roles to check is a string, currentUser.roles is a string
+ return currentUserRoles === roles
+ } else if (Array.isArray(currentUserRoles)) {
+ // roles to check is a string, currentUser.roles is an array
+ return currentUserRoles?.some((allowedRole) => roles === allowedRole)
+ }
+ }
+
+ if (Array.isArray(roles)) {
+ if (Array.isArray(currentUserRoles)) {
+ // roles to check is an array, currentUser.roles is an array
+ return currentUserRoles?.some((allowedRole) =>
+ roles.includes(allowedRole)
+ )
+ } else if (typeof currentUserRoles === 'string') {
+ // roles to check is an array, currentUser.roles is a string
+ return roles.some((allowedRole) => currentUserRoles === allowedRole)
+ }
+ }
+
+ // roles not found
+ return false
+}
+
+/**
+ * Use requireAuth in your services to check that a user is logged in,
+ * whether or not they are assigned a role, and optionally raise an
+ * error if they're not.
+ *
+ * @param roles: {@link AllowedRoles} - When checking role membership, these roles grant access.
+ *
+ * @returns - If the currentUser is authenticated (and assigned one of the given roles)
+ *
+ * @throws {@link AuthenticationError} - If the currentUser is not authenticated
+ * @throws {@link ForbiddenError} If the currentUser is not allowed due to role permissions
+ *
+ * @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
+ */
+export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => {
+ if (!isAuthenticated()) {
+ throw new AuthenticationError("You don't have permission to do that.")
+ }
+
+ if (roles && !hasRole(roles)) {
+ throw new ForbiddenError("You don't have access to do that.")
+ }
}
diff --git a/web/package.json b/web/package.json
index bfe3a5d..c2a7286 100644
--- a/web/package.json
+++ b/web/package.json
@@ -13,6 +13,7 @@
"dependencies": {
"@mdi/js": "^7.3.67",
"@mdi/react": "^1.6.1",
+ "@redwoodjs/auth-dbauth-web": "6.3.2",
"@redwoodjs/forms": "6.3.2",
"@redwoodjs/router": "6.3.2",
"@redwoodjs/web": "6.3.2",
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 5e7beac..65419d6 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -4,15 +4,19 @@ import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'
+import { AuthProvider, useAuth } from './auth'
+
import './scaffold.css'
import './index.css'
const App = () => (
-
-
-
+
+
+
+
+
)
diff --git a/web/src/Routes.tsx b/web/src/Routes.tsx
index f7ce2cd..42cdee9 100644
--- a/web/src/Routes.tsx
+++ b/web/src/Routes.tsx
@@ -7,20 +7,28 @@
// 'src/pages/HomePage/HomePage.js' -> HomePage
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
-import { Router, Route, Set } from '@redwoodjs/router'
+import { Router, Route, Set, Private } from '@redwoodjs/router'
import NavbarLayout from 'src/layouts/NavbarLayout'
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
+import { useAuth } from './auth'
+
const Routes = () => {
return (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/auth.ts b/web/src/auth.ts
new file mode 100644
index 0000000..143e75b
--- /dev/null
+++ b/web/src/auth.ts
@@ -0,0 +1,5 @@
+import { createDbAuthClient, createAuth } from '@redwoodjs/auth-dbauth-web'
+
+const dbAuthClient = createDbAuthClient()
+
+export const { AuthProvider, useAuth } = createAuth(dbAuthClient)
diff --git a/web/src/components/NavbarAccountIcon/NavbarAccountIcon.stories.tsx b/web/src/components/NavbarAccountIcon/NavbarAccountIcon.stories.tsx
new file mode 100644
index 0000000..01336fc
--- /dev/null
+++ b/web/src/components/NavbarAccountIcon/NavbarAccountIcon.stories.tsx
@@ -0,0 +1,25 @@
+// Pass props to your component by passing an `args` object to your story
+//
+// ```tsx
+// export const Primary: Story = {
+// args: {
+// propName: propValue
+// }
+// }
+// ```
+//
+// See https://storybook.js.org/docs/react/writing-stories/args.
+
+import type { Meta, StoryObj } from '@storybook/react'
+
+import NavbarAccountIcon from './NavbarAccountIcon'
+
+const meta: Meta = {
+ component: NavbarAccountIcon,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/web/src/components/NavbarAccountIcon/NavbarAccountIcon.test.tsx b/web/src/components/NavbarAccountIcon/NavbarAccountIcon.test.tsx
new file mode 100644
index 0000000..b1a6d52
--- /dev/null
+++ b/web/src/components/NavbarAccountIcon/NavbarAccountIcon.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@redwoodjs/testing/web'
+
+import NavbarAccountIcon from './NavbarAccountIcon'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-components
+
+describe('NavbarAccountIcon', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render()
+ }).not.toThrow()
+ })
+})
diff --git a/web/src/components/NavbarAccountIcon/NavbarAccountIcon.tsx b/web/src/components/NavbarAccountIcon/NavbarAccountIcon.tsx
new file mode 100644
index 0000000..9a3cd1f
--- /dev/null
+++ b/web/src/components/NavbarAccountIcon/NavbarAccountIcon.tsx
@@ -0,0 +1,47 @@
+import { mdiAccount, mdiLogout, mdiLogin } from '@mdi/js'
+import Icon from '@mdi/react'
+
+import { Link, routes } from '@redwoodjs/router'
+
+import { useAuth } from 'src/auth'
+
+interface Props {
+ mobile: boolean
+ className?: string
+}
+
+const NavbarAccountIcon = ({ mobile, className }: Props) => {
+ const { isAuthenticated, currentUser, logOut } = useAuth()
+
+ return isAuthenticated ? (
+
+
+
+
+
+
+
+ Hello, {currentUser.firstName}!
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ )
+}
+
+export default NavbarAccountIcon
diff --git a/web/src/layouts/NavbarLayout/NavbarLayout.tsx b/web/src/layouts/NavbarLayout/NavbarLayout.tsx
index 93fa53c..92e714d 100644
--- a/web/src/layouts/NavbarLayout/NavbarLayout.tsx
+++ b/web/src/layouts/NavbarLayout/NavbarLayout.tsx
@@ -3,6 +3,7 @@ import Icon from '@mdi/react'
import { Link, routes } from '@redwoodjs/router'
+import NavbarAccountIcon from 'src/components/NavbarAccountIcon/NavbarAccountIcon'
import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
type NavBarLayoutProps = {
@@ -14,7 +15,10 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
<>
-
+
{
*/}
+
{
-
+
{
Parts Inventory
+
{/*
diff --git a/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx b/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
new file mode 100644
index 0000000..1dce40c
--- /dev/null
+++ b/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
@@ -0,0 +1,96 @@
+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: { email: string }) => {
+ 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 (
+ <>
+
+
+
+
+