Adds dbAuth
This commit is contained in:
149
api/src/functions/auth.js
Normal file
149
api/src/functions/auth.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { db } from 'src/lib/db'
|
||||||
|
import { DbAuthHandler } from '@redwoodjs/api'
|
||||||
|
|
||||||
|
export const handler = async (event, context) => {
|
||||||
|
const forgotPasswordOptions = {
|
||||||
|
// 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) => {
|
||||||
|
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: 'Username not found',
|
||||||
|
// if the user somehow gets around client validation
|
||||||
|
usernameRequired: 'Username is required',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginOptions = {
|
||||||
|
// 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 username and password are required',
|
||||||
|
usernameNotFound: 'Username ${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 = {
|
||||||
|
// handler() is invoked after the password has been successfully updated in
|
||||||
|
// the database. Returning anything truthy will automatically logs the user
|
||||||
|
// in. Return `false` otherwise, and in the Reset Password page redirect the
|
||||||
|
// user to the login page.
|
||||||
|
handler: (user) => {
|
||||||
|
return user
|
||||||
|
},
|
||||||
|
|
||||||
|
// If `false` then the new password MUST be different than the current one
|
||||||
|
allowReusedPassword: true,
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
// 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,
|
||||||
|
// name: userAttributes.name
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
// `field` will be either "username" or "password"
|
||||||
|
fieldMissing: '${field} is required',
|
||||||
|
usernameTaken: 'Username `${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.
|
||||||
|
// ie. 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',
|
||||||
|
},
|
||||||
|
|
||||||
|
forgotPassword: forgotPasswordOptions,
|
||||||
|
login: loginOptions,
|
||||||
|
resetPassword: resetPasswordOptions,
|
||||||
|
signup: signupOptions,
|
||||||
|
})
|
||||||
|
|
||||||
|
return await authHandler.invoke()
|
||||||
|
}
|
||||||
@@ -1,36 +1,28 @@
|
|||||||
import { parseJWT } from '@redwoodjs/api'
|
|
||||||
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
|
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
|
||||||
import { logger } from 'src/lib/logger'
|
import { db } from './db'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getCurrentUser returns the user information together with
|
* The session object sent in as the first argument to getCurrentUser() will
|
||||||
* an optional collection of roles used by requireAuth() to check
|
* have a single key `id` containing the unique ID of the logged in user
|
||||||
* if the user is authenticated or has role-based access
|
* (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:
|
||||||
*
|
*
|
||||||
* @param decoded - The decoded access token containing user info and JWT claims like `sub`. Note could be null.
|
* return await db.profile.findUnique({ where: { email: session.id } })
|
||||||
* @param { token, SupportedAuthTypes type } - The access token itself as well as the auth provider type
|
* ───┬─── ──┬──
|
||||||
* @param { APIGatewayEvent event, Context context } - An object which contains information from the invoker
|
* model accessor ─┘ unique id field name ─┘
|
||||||
* such as headers and cookies, and the context information about the invocation such as IP Address
|
|
||||||
*
|
*
|
||||||
* @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
|
* !! 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 getCurrentUser = async (
|
export const getCurrentUser = async (session) => {
|
||||||
decoded,
|
return await db.user.findUnique({
|
||||||
{ _token, _type },
|
where: { id: session.id },
|
||||||
{ _event, _context }
|
select: { id: true },
|
||||||
) => {
|
})
|
||||||
if (!decoded) {
|
|
||||||
logger.warn('Missing decoded user')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const { roles } = parseJWT({ decoded })
|
|
||||||
|
|
||||||
if (roles) {
|
|
||||||
return { ...decoded, roles }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...decoded }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,13 +52,14 @@ export const hasRole = ({ roles }) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If your User model includes roles, uncomment the role checks on currentUser
|
||||||
if (roles) {
|
if (roles) {
|
||||||
if (Array.isArray(roles)) {
|
if (Array.isArray(roles)) {
|
||||||
return context.currentUser.roles?.some((r) => roles.includes(r))
|
// return context.currentUser.roles?.some((r) => roles.includes(r))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof roles === 'string') {
|
if (typeof roles === 'string') {
|
||||||
return context.currentUser.roles?.includes(roles)
|
// return context.currentUser.roles?.includes(roles)
|
||||||
}
|
}
|
||||||
|
|
||||||
// roles not found
|
// roles not found
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import { AuthProvider } from '@redwoodjs/auth'
|
import { AuthProvider } from '@redwoodjs/auth'
|
||||||
import netlifyIdentity from 'netlify-identity-widget'
|
|
||||||
|
import { AuthProvider } from '@redwoodjs/auth'
|
||||||
import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
|
import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
|
||||||
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
|
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
|
||||||
import FatalErrorPage from 'src/pages/FatalErrorPage'
|
import FatalErrorPage from 'src/pages/FatalErrorPage'
|
||||||
import { isBrowser } from '@redwoodjs/prerender/browserUtils'
|
|
||||||
|
|
||||||
import Routes from 'src/Routes'
|
import Routes from 'src/Routes'
|
||||||
|
|
||||||
import './scaffold.css'
|
import './scaffold.css'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
isBrowser && netlifyIdentity.init()
|
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<FatalErrorBoundary page={FatalErrorPage}>
|
<FatalErrorBoundary page={FatalErrorPage}>
|
||||||
<RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
|
<RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
|
||||||
<AuthProvider client={netlifyIdentity} type="netlify">
|
<AuthProvider type="dbAuth">
|
||||||
<RedwoodApolloProvider>
|
<RedwoodApolloProvider>
|
||||||
<Routes />
|
<Routes />
|
||||||
</RedwoodApolloProvider>
|
</RedwoodApolloProvider>
|
||||||
|
|||||||
@@ -14,12 +14,10 @@ import BlogLayout from 'src/layouts/BlogLayout'
|
|||||||
const Routes = () => {
|
const Routes = () => {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Set wrap={BlogLayout}>
|
<Route path="/login" page={LoginPage} name="login" />
|
||||||
<Route path="/contact" page={ContactPage} name="contact" />
|
<Route path="/signup" page={SignupPage} name="signup" />
|
||||||
<Route path="/blog-post/{id:Int}" page={BlogPostPage} name="blogPost" />
|
<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />
|
||||||
<Route path="/about" page={AboutPage} name="about" />
|
<Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />
|
||||||
<Route path="/" page={HomePage} name="home" />
|
|
||||||
</Set>
|
|
||||||
<Private unauthenticated="home">
|
<Private unauthenticated="home">
|
||||||
<Set wrap={PostsLayout}>
|
<Set wrap={PostsLayout}>
|
||||||
<Route path="/admin/posts/new" page={PostNewPostPage} name="newPost" />
|
<Route path="/admin/posts/new" page={PostNewPostPage} name="newPost" />
|
||||||
|
|||||||
89
web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js
Normal file
89
web/src/pages/ForgotPasswordPage/ForgotPasswordPage.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
|
import { navigate, routes } from '@redwoodjs/router'
|
||||||
|
import { MetaTags } from '@redwoodjs/web'
|
||||||
|
import { toast, Toaster } from '@redwoodjs/web/toast'
|
||||||
|
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
|
||||||
|
|
||||||
|
const ForgotPasswordPage = () => {
|
||||||
|
const { isAuthenticated, forgotPassword } = useAuth()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
navigate(routes.home())
|
||||||
|
}
|
||||||
|
}, [isAuthenticated])
|
||||||
|
|
||||||
|
const usernameRef = useRef()
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<MetaTags title="Forgot Password" />
|
||||||
|
|
||||||
|
<main className="rw-main">
|
||||||
|
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
|
||||||
|
<div className="rw-scaffold rw-login-container">
|
||||||
|
<div className="rw-segment">
|
||||||
|
<header className="rw-segment-header">
|
||||||
|
<h2 className="rw-heading rw-heading-secondary">
|
||||||
|
Forgot Password
|
||||||
|
</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="rw-segment-main">
|
||||||
|
<div className="rw-form-wrapper">
|
||||||
|
<Form onSubmit={onSubmit} className="rw-form-wrapper">
|
||||||
|
<div className="text-left">
|
||||||
|
<Label
|
||||||
|
name="username"
|
||||||
|
className="rw-label"
|
||||||
|
errorClassName="rw-label rw-label-error"
|
||||||
|
>
|
||||||
|
Username
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
name="username"
|
||||||
|
className="rw-input"
|
||||||
|
errorClassName="rw-input rw-input-error"
|
||||||
|
ref={usernameRef}
|
||||||
|
validation={{
|
||||||
|
required: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FieldError name="username" className="rw-field-error" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rw-button-group">
|
||||||
|
<Submit className="rw-button rw-button-blue">Submit</Submit>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ForgotPasswordPage
|
||||||
129
web/src/pages/LoginPage/LoginPage.js
Normal file
129
web/src/pages/LoginPage/LoginPage.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { Link, navigate, routes } from '@redwoodjs/router'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
Label,
|
||||||
|
TextField,
|
||||||
|
PasswordField,
|
||||||
|
Submit,
|
||||||
|
FieldError,
|
||||||
|
} from '@redwoodjs/forms'
|
||||||
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
|
import { MetaTags } from '@redwoodjs/web'
|
||||||
|
import { toast, Toaster } from '@redwoodjs/web/toast'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
const { isAuthenticated, logIn } = useAuth()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
navigate(routes.home())
|
||||||
|
}
|
||||||
|
}, [isAuthenticated])
|
||||||
|
|
||||||
|
const usernameRef = useRef()
|
||||||
|
useEffect(() => {
|
||||||
|
usernameRef.current.focus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
const response = await logIn({ ...data })
|
||||||
|
|
||||||
|
if (response.message) {
|
||||||
|
toast(response.message)
|
||||||
|
} else if (response.error) {
|
||||||
|
toast.error(response.error)
|
||||||
|
} else {
|
||||||
|
toast.success('Welcome back!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MetaTags title="Login" />
|
||||||
|
|
||||||
|
<main className="rw-main">
|
||||||
|
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
|
||||||
|
<div className="rw-scaffold rw-login-container">
|
||||||
|
<div className="rw-segment">
|
||||||
|
<header className="rw-segment-header">
|
||||||
|
<h2 className="rw-heading rw-heading-secondary">Login</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="rw-segment-main">
|
||||||
|
<div className="rw-form-wrapper">
|
||||||
|
<Form onSubmit={onSubmit} className="rw-form-wrapper">
|
||||||
|
<Label
|
||||||
|
name="username"
|
||||||
|
className="rw-label"
|
||||||
|
errorClassName="rw-label rw-label-error"
|
||||||
|
>
|
||||||
|
Username
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
name="username"
|
||||||
|
className="rw-input"
|
||||||
|
errorClassName="rw-input rw-input-error"
|
||||||
|
ref={usernameRef}
|
||||||
|
validation={{
|
||||||
|
required: {
|
||||||
|
value: true,
|
||||||
|
message: 'Username is required',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FieldError name="username" className="rw-field-error" />
|
||||||
|
|
||||||
|
<Label
|
||||||
|
name="password"
|
||||||
|
className="rw-label"
|
||||||
|
errorClassName="rw-label rw-label-error"
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
</Label>
|
||||||
|
<PasswordField
|
||||||
|
name="password"
|
||||||
|
className="rw-input"
|
||||||
|
errorClassName="rw-input rw-input-error"
|
||||||
|
autoComplete="current-password"
|
||||||
|
validation={{
|
||||||
|
required: {
|
||||||
|
value: true,
|
||||||
|
message: 'Password is required',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="rw-forgot-link">
|
||||||
|
<Link
|
||||||
|
to={routes.forgotPassword()}
|
||||||
|
className="rw-forgot-link"
|
||||||
|
>
|
||||||
|
Forgot Password?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FieldError name="password" className="rw-field-error" />
|
||||||
|
|
||||||
|
<div className="rw-button-group">
|
||||||
|
<Submit className="rw-button rw-button-blue">Login</Submit>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rw-login-link">
|
||||||
|
<span>Don't have an account?</span>{' '}
|
||||||
|
<Link to={routes.signup()} className="rw-link">
|
||||||
|
Sign up!
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginPage
|
||||||
119
web/src/pages/ResetPasswordPage/ResetPasswordPage.js
Normal file
119
web/src/pages/ResetPasswordPage/ResetPasswordPage.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
|
import { navigate, routes } from '@redwoodjs/router'
|
||||||
|
import { MetaTags } from '@redwoodjs/web'
|
||||||
|
import { toast, Toaster } from '@redwoodjs/web/toast'
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
Label,
|
||||||
|
PasswordField,
|
||||||
|
Submit,
|
||||||
|
FieldError,
|
||||||
|
} from '@redwoodjs/forms'
|
||||||
|
|
||||||
|
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()
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<MetaTags title="Reset Password" />
|
||||||
|
|
||||||
|
<main className="rw-main">
|
||||||
|
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
|
||||||
|
<div className="rw-scaffold rw-login-container">
|
||||||
|
<div className="rw-segment">
|
||||||
|
<header className="rw-segment-header">
|
||||||
|
<h2 className="rw-heading rw-heading-secondary">
|
||||||
|
Reset Password
|
||||||
|
</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="rw-segment-main">
|
||||||
|
<div className="rw-form-wrapper">
|
||||||
|
<Form onSubmit={onSubmit} className="rw-form-wrapper">
|
||||||
|
<div className="text-left">
|
||||||
|
<Label
|
||||||
|
name="password"
|
||||||
|
className="rw-label"
|
||||||
|
errorClassName="rw-label rw-label-error"
|
||||||
|
>
|
||||||
|
New Password
|
||||||
|
</Label>
|
||||||
|
<PasswordField
|
||||||
|
name="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
className="rw-input"
|
||||||
|
errorClassName="rw-input rw-input-error"
|
||||||
|
disabled={!enabled}
|
||||||
|
ref={passwordRef}
|
||||||
|
validation={{
|
||||||
|
required: {
|
||||||
|
value: true,
|
||||||
|
message: 'Password is required',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FieldError name="password" className="rw-field-error" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rw-button-group">
|
||||||
|
<Submit
|
||||||
|
className="rw-button rw-button-blue"
|
||||||
|
disabled={!enabled}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Submit>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResetPasswordPage
|
||||||
124
web/src/pages/SignupPage/SignupPage.js
Normal file
124
web/src/pages/SignupPage/SignupPage.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { Link, navigate, routes } from '@redwoodjs/router'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
Label,
|
||||||
|
TextField,
|
||||||
|
PasswordField,
|
||||||
|
FieldError,
|
||||||
|
Submit,
|
||||||
|
} from '@redwoodjs/forms'
|
||||||
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
|
import { MetaTags } from '@redwoodjs/web'
|
||||||
|
import { toast, Toaster } from '@redwoodjs/web/toast'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
const SignupPage = () => {
|
||||||
|
const { isAuthenticated, signUp } = useAuth()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
navigate(routes.home())
|
||||||
|
}
|
||||||
|
}, [isAuthenticated])
|
||||||
|
|
||||||
|
// focus on email box on page load
|
||||||
|
const usernameRef = useRef()
|
||||||
|
useEffect(() => {
|
||||||
|
usernameRef.current.focus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
const response = await signUp({ ...data })
|
||||||
|
|
||||||
|
if (response.message) {
|
||||||
|
toast(response.message)
|
||||||
|
} else if (response.error) {
|
||||||
|
toast.error(response.error)
|
||||||
|
} else {
|
||||||
|
// user is signed in automatically
|
||||||
|
toast.success('Welcome!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MetaTags title="Signup" />
|
||||||
|
|
||||||
|
<main className="rw-main">
|
||||||
|
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
|
||||||
|
<div className="rw-scaffold rw-login-container">
|
||||||
|
<div className="rw-segment">
|
||||||
|
<header className="rw-segment-header">
|
||||||
|
<h2 className="rw-heading rw-heading-secondary">Signup</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="rw-segment-main">
|
||||||
|
<div className="rw-form-wrapper">
|
||||||
|
<Form onSubmit={onSubmit} className="rw-form-wrapper">
|
||||||
|
<Label
|
||||||
|
name="username"
|
||||||
|
className="rw-label"
|
||||||
|
errorClassName="rw-label rw-label-error"
|
||||||
|
>
|
||||||
|
Username
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
name="username"
|
||||||
|
className="rw-input"
|
||||||
|
errorClassName="rw-input rw-input-error"
|
||||||
|
ref={usernameRef}
|
||||||
|
validation={{
|
||||||
|
required: {
|
||||||
|
value: true,
|
||||||
|
message: 'Username is required',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FieldError name="username" className="rw-field-error" />
|
||||||
|
|
||||||
|
<Label
|
||||||
|
name="password"
|
||||||
|
className="rw-label"
|
||||||
|
errorClassName="rw-label rw-label-error"
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
</Label>
|
||||||
|
<PasswordField
|
||||||
|
name="password"
|
||||||
|
className="rw-input"
|
||||||
|
errorClassName="rw-input rw-input-error"
|
||||||
|
autoComplete="current-password"
|
||||||
|
validation={{
|
||||||
|
required: {
|
||||||
|
value: true,
|
||||||
|
message: 'Password is required',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FieldError name="password" className="rw-field-error" />
|
||||||
|
|
||||||
|
<div className="rw-button-group">
|
||||||
|
<Submit className="rw-button rw-button-blue">
|
||||||
|
Sign Up
|
||||||
|
</Submit>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rw-login-link">
|
||||||
|
<span>Already have an account?</span>{' '}
|
||||||
|
<Link to={routes.login()} className="rw-link">
|
||||||
|
Log in!
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignupPage
|
||||||
Reference in New Issue
Block a user