login/logout functionality

This commit is contained in:
Graeme Ross 2024-10-15 20:11:05 +01:00
parent 7da292aa48
commit 64736977cd
17 changed files with 1107 additions and 131 deletions

View File

@ -0,0 +1,35 @@
/*
Warnings:
- You are about to drop the column `contactAddressId` on the `User` table. All the data in the column will be lost.
- Added the required column `hashedPassword` to the `User` table without a default value. This is not possible if the table is not empty.
- Added the required column `salt` to the `User` table without a default value. This is not possible if the table is not empty.
*/
-- CreateTable
CREATE TABLE "UserToContactAddress" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"userId" TEXT NOT NULL,
"contactAddressId" TEXT NOT NULL,
CONSTRAINT "UserToContactAddress_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "UserToContactAddress_contactAddressId_fkey" FOREIGN KEY ("contactAddressId") REFERENCES "ContactAddress" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_User" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"email" TEXT NOT NULL,
"hashedPassword" TEXT NOT NULL,
"salt" TEXT NOT NULL,
"resetToken" TEXT,
"resetTokenExpiresAt" DATETIME
);
INSERT INTO "new_User" ("email", "id", "userId") SELECT "email", "id", "userId" FROM "User";
DROP TABLE "User";
ALTER TABLE "new_User" RENAME TO "User";
CREATE UNIQUE INDEX "User_userId_key" ON "User"("userId");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -38,17 +38,28 @@ model ContactAddress {
Town String Town String
PostalCode String PostalCode String
Account Account[] Account Account[]
User User[] UserToContactAddress UserToContactAddress[]
} }
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
userId String @unique userId String @unique
email String email String
address ContactAddress @relation(fields: [contactAddressId], references: [id]) hashedPassword String
contactAddressId String salt String
resetToken String?
resetTokenExpiresAt DateTime?
accounts Account[] accounts Account[]
roles Role[] roles Role[]
UserToContactAddress UserToContactAddress[]
}
model UserToContactAddress {
id Int @id
user User @relation(fields: [userId], references: [id])
address ContactAddress @relation(fields: [contactAddressId], references: [id])
userId String
contactAddressId String
} }
model Role { model Role {

View File

@ -4,6 +4,7 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@redwoodjs/api": "8.3.0", "@redwoodjs/api": "8.3.0",
"@redwoodjs/auth-dbauth-api": "8.4.0",
"@redwoodjs/graphql-server": "8.3.0" "@redwoodjs/graphql-server": "8.3.0"
} }
} }

208
api/src/functions/auth.ts Normal file
View File

@ -0,0 +1,208 @@
import type { APIGatewayProxyEvent, Context } from 'aws-lambda'
import { DbAuthHandler } from '@redwoodjs/auth-dbauth-api'
import type { DbAuthHandlerOptions, UserType } from '@redwoodjs/auth-dbauth-api'
import { cookieName } from 'src/lib/auth'
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.
//
// Note that this return value is sent to the client in *plain text*
// so don't include anything you wouldn't want prying eyes to see. The
// `user` here has been sanitized to only include the fields listed in
// `allowedUserFields` so it should be safe to return as-is.
handler: (user, _resetToken) => {
// TODO: Send user an email/message with a link to reset their password,
// including the `resetToken`. The URL should look something like:
// `http://localhost:8910/reset-password?resetToken=${resetToken}`
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: 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 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: 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: 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',
},
}
interface UserAttributes {
name: string
}
const signupOptions: DbAuthHandlerOptions<
UserType,
UserAttributes
>['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: _userAttributes,
}) => {
return db.user.create({
data: {
email: username,
userId: username,
hashedPassword: hashedPassword,
salt: salt,
// name: userAttributes.name
},
})
},
// 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) => {
return true
},
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.
// 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',
},
// A list of fields on your user object that are safe to return to the
// client when invoking a handler that returns a user (like forgotPassword
// and signup). This list should be as small as possible to be sure not to
// leak any sensitive information to the client.
allowedUserFields: ['id', 'email'],
// 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: {
attributes: {
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',
},
name: cookieName,
},
forgotPassword: forgotPasswordOptions,
login: loginOptions,
resetPassword: resetPasswordOptions,
signup: signupOptions,
})
return await authHandler.invoke()
}

View File

@ -1,13 +1,19 @@
import { createAuthDecoder } from '@redwoodjs/auth-dbauth-api'
import { createGraphQLHandler } from '@redwoodjs/graphql-server' import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import directives from 'src/directives/**/*.{js,ts}' import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}' import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}' import services from 'src/services/**/*.{js,ts}'
import { cookieName, getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db' import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger' import { logger } from 'src/lib/logger'
const authDecoder = createAuthDecoder(cookieName)
export const handler = createGraphQLHandler({ export const handler = createGraphQLHandler({
authDecoder,
getCurrentUser,
loggerConfig: { logger, options: {} }, loggerConfig: { logger, options: {} },
directives, directives,
sdls, sdls,

View File

@ -1,32 +1,121 @@
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 * The name of the cookie that dbAuth sets
* 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.
* *
* See https://redwoodjs.com/docs/authentication for more info. * %port% will be replaced with the port the api server is running on.
* If you have multiple RW apps running on the same host, you'll need to
* make sure they all use unique cookie names
*/ */
export const isAuthenticated = () => { export const cookieName = 'session_%port%'
return true
/**
* 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:
*
* 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 getCurrentUser = async (session: Decoded) => {
if (!session || typeof session.id !== 'string') {
throw new Error('Invalid session')
}
return await db.user.findUnique({
where: { id: session.id },
select: { id: 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 @typescript-eslint/no-unused-vars * Checks if the currentUser is authenticated (and assigned one of the given roles)
export const requireAuth = ({ roles }) => { *
return isAuthenticated() * @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
}
export const getCurrentUser = async () => { const currentUserRoles = context.currentUser?.roles
throw new Error(
'Auth is not set up yet. See https://redwoodjs.com/docs/authentication ' + if (typeof roles === 'string') {
'to get started' 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.")
}
} }

View File

@ -7,6 +7,7 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@redwoodjs/auth-dbauth-setup": "8.4.0",
"@redwoodjs/core": "8.4.0", "@redwoodjs/core": "8.4.0",
"@redwoodjs/project-config": "8.4.0", "@redwoodjs/project-config": "8.4.0",
"prettier-plugin-tailwindcss": "^0.6.8" "prettier-plugin-tailwindcss": "^0.6.8"

View File

@ -11,6 +11,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@redwoodjs/auth-dbauth-web": "8.4.0",
"@redwoodjs/forms": "8.4.0", "@redwoodjs/forms": "8.4.0",
"@redwoodjs/router": "8.4.0", "@redwoodjs/router": "8.4.0",
"@redwoodjs/web": "8.4.0", "@redwoodjs/web": "8.4.0",

View File

@ -5,6 +5,8 @@ import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
import FatalErrorPage from 'src/pages/FatalErrorPage' import FatalErrorPage from 'src/pages/FatalErrorPage'
import { AuthProvider, useAuth } from './auth'
import './index.css' import './index.css'
import './scaffold.css' import './scaffold.css'
@ -16,7 +18,9 @@ interface AppProps {
const App = ({ children }: AppProps) => ( const App = ({ children }: AppProps) => (
<FatalErrorBoundary page={FatalErrorPage}> <FatalErrorBoundary page={FatalErrorPage}>
<RedwoodProvider titleTemplate="%PageTitle | %AppTitle"> <RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
<RedwoodApolloProvider>{children}</RedwoodApolloProvider> <AuthProvider>
<RedwoodApolloProvider useAuth={useAuth}>{children}</RedwoodApolloProvider>
</AuthProvider>
</RedwoodProvider> </RedwoodProvider>
</FatalErrorBoundary> </FatalErrorBoundary>
) )

View File

@ -9,13 +9,18 @@
import { Router, Route, Set } from '@redwoodjs/router' import { Router, Route, Set } from '@redwoodjs/router'
import ClientLayout from 'src/layouts/ClientLayout/ClientLayout'
import ScaffoldLayout from 'src/layouts/ScaffoldLayout' import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
import ClientLayout from 'src/layouts/ClientLayout/ClientLayout' import { useAuth } from './auth'
const Routes = () => { const Routes = () => {
return ( return (
<Router> <Router useAuth={useAuth}>
<Route path="/login" page={LoginPage} name="login" />
<Route path="/signup" page={SignupPage} name="signup" />
<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />
<Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />
<Set wrap={ScaffoldLayout} title="Roles" titleTo="roles" buttonLabel="New Role" buttonTo="newRole"> <Set wrap={ScaffoldLayout} title="Roles" titleTo="roles" buttonLabel="New Role" buttonTo="newRole">
<Route path="/roles/new" page={RoleNewRolePage} name="newRole" /> <Route path="/roles/new" page={RoleNewRolePage} name="newRole" />
<Route path="/roles/{id}/edit" page={RoleEditRolePage} name="editRole" /> <Route path="/roles/{id}/edit" page={RoleEditRolePage} name="editRole" />

5
web/src/auth.ts Normal file
View File

@ -0,0 +1,5 @@
import { createDbAuthClient, createAuth } from '@redwoodjs/auth-dbauth-web'
const dbAuthClient = createDbAuthClient()
export const { AuthProvider, useAuth } = createAuth(dbAuthClient)

View File

@ -1,4 +1,6 @@
import { Link, routes } from '@redwoodjs/router' import { Link, routes } from '@redwoodjs/router'
import { useAuth } from 'src/auth'
import ThemeChanger from 'src/components/ThemeChanger/ThemeChanger' import ThemeChanger from 'src/components/ThemeChanger/ThemeChanger'
type ClientLayoutProps = { type ClientLayoutProps = {
@ -6,20 +8,35 @@ type ClientLayoutProps = {
} }
const ClientLayout = ({ children }: ClientLayoutProps) => { const ClientLayout = ({ children }: ClientLayoutProps) => {
const { logOut } = useAuth()
return ( return (
<> <>
<div className="drawer fixed top-20"> <div className="drawer fixed top-20">
<input id="my-drawer" type="checkbox" className="drawer-toggle" /> <input id="my-drawer" type="checkbox" className="drawer-toggle" />
<div className="drawer-content"> <div className="drawer-content">
{/* Page content here */} {/* Page content here */}
<label htmlFor="my-drawer" className="btn btn-square btn-xs drawer-button">&gt;&gt;&gt;</label> <label
htmlFor="my-drawer"
className="btn btn-square drawer-button btn-xs"
>
&gt;&gt;&gt;
</label>
</div> </div>
<div className="drawer-side"> <div className="drawer-side">
<label htmlFor="my-drawer" aria-label="close sidebar" className="drawer-overlay"></label> <label
<ul className="menu bg-base-200 text-base-content min-h-full w-80 p-4"> htmlFor="my-drawer"
aria-label="close sidebar"
className="drawer-overlay"
></label>
<ul className="menu min-h-full w-80 bg-base-200 p-4 text-base-content">
{/* Sidebar content here */} {/* Sidebar content here */}
<li><a>Sidebar Item 1</a></li> <li>
<li><a>Sidebar Item 2</a></li> <a>Sidebar Item 1</a>
</li>
<li>
<a>Sidebar Item 2</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>
@ -29,39 +46,48 @@ const ClientLayout = ({ children }: ClientLayoutProps) => {
</div> </div>
<div className="flex-none"> <div className="flex-none">
<ul className="menu menu-horizontal px-1"> <ul className="menu menu-horizontal px-1">
<li><Link to={routes.home()}>Home</Link></li> <li>
<Link to={routes.home()}>Home</Link>
</li>
<li> <li>
<details> <details>
<summary>Parent</summary> <summary>Parent</summary>
<ul className="bg-base-100 rounded-t-none p-2"> <ul className="rounded-t-none bg-base-100 p-2">
<li><a>Link 1</a></li> <li>
<li><a>Link 2</a></li> <a>Link 1</a>
</li>
<li>
<a>Link 2</a>
</li>
</ul> </ul>
</details> </details>
</li> </li>
</ul> </ul>
</div> </div>
<div className="dropdown dropdown-end"> <div className="dropdown dropdown-end">
<div tabIndex={0} role="button" className="btn btn-ghost btn-circle"> <div tabIndex={0} role="button" className="btn btn-circle btn-ghost">
<div className="indicator"> <div className="indicator">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5" className="h-5 w-5"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor"
>
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="2" strokeWidth="2"
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" /> d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg> </svg>
<span className="badge badge-sm indicator-item">8</span> <span className="badge indicator-item badge-sm">8</span>
</div> </div>
</div> </div>
<div <div
tabIndex={0} tabIndex={0}
className="card card-compact dropdown-content bg-base-100 z-[1] mt-3 w-52 shadow"> className="card dropdown-content card-compact z-[1] mt-3 w-52 bg-base-100 shadow"
>
<div className="card-body"> <div className="card-body">
<span className="text-lg font-bold">8 Items</span> <span className="text-lg font-bold">8 Items</span>
<span className="text-info">Subtotal: $999</span> <span className="text-info">Subtotal: $999</span>
@ -72,55 +98,68 @@ const ClientLayout = ({ children }: ClientLayoutProps) => {
</div> </div>
</div> </div>
<div className="dropdown dropdown-end"> <div className="dropdown dropdown-end">
<div tabIndex={0} role="button" className="btn btn-ghost btn-circle avatar"> <div
tabIndex={0}
role="button"
className="avatar btn btn-circle btn-ghost"
>
<div className="w-10 rounded-full"> <div className="w-10 rounded-full">
<img <img
alt="Tailwind CSS Navbar component" alt="Tailwind CSS Navbar component"
src="https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.webp" /> src="https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.webp"
/>
</div> </div>
</div> </div>
<ul <ul
tabIndex={0} tabIndex={0}
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"> className="menu dropdown-content menu-sm z-[1] mt-3 w-52 rounded-box bg-base-100 p-2 shadow"
>
<li> <li>
<a className="justify-between"> <a className="justify-between">
Profile Profile
<span className="badge">New</span> <span className="badge">New</span>
</a> </a>
</li> </li>
<li><a>Settings</a></li> <li>
<li><ThemeChanger/></li> <a>Settings</a>
<li><a>Logout</a></li> </li>
<li>
<ThemeChanger />
</li>
<li>
<Link onClick={logOut} to={routes.home()}>
Logout
</Link>
</li>
</ul> </ul>
</div> </div>
</div> </div>
<div> <div>
<main>{children}</main> <main>{children}</main>
</div> </div>
<footer className="footer bg-base-200 text-base-content p-10"> <footer className="footer bg-base-200 p-10 text-base-content">
<nav> <nav>
<h6 className="footer-title">Services</h6> <h6 className="footer-title">Services</h6>
<a className="link link-hover">Branding</a> <a className="link-hover link">Branding</a>
<a className="link link-hover">Design</a> <a className="link-hover link">Design</a>
<a className="link link-hover">Marketing</a> <a className="link-hover link">Marketing</a>
<a className="link link-hover">Advertisement</a> <a className="link-hover link">Advertisement</a>
</nav> </nav>
<nav> <nav>
<h6 className="footer-title">Company</h6> <h6 className="footer-title">Company</h6>
<a className="link link-hover">About us</a> <a className="link-hover link">About us</a>
<a className="link link-hover">Contact</a> <a className="link-hover link">Contact</a>
<a className="link link-hover">Jobs</a> <a className="link-hover link">Jobs</a>
<a className="link link-hover">Press kit</a> <a className="link-hover link">Press kit</a>
</nav> </nav>
<nav> <nav>
<h6 className="footer-title">Legal</h6> <h6 className="footer-title">Legal</h6>
<a className="link link-hover">Terms of use</a> <a className="link-hover link">Terms of use</a>
<a className="link link-hover">Privacy policy</a> <a className="link-hover link">Privacy policy</a>
<a className="link link-hover">Cookie policy</a> <a className="link-hover link">Cookie policy</a>
</nav> </nav>
</footer> </footer>
<footer className="fixed bottom-0 footer bg-base-200 text-base-content border-base-300 border-t px-10 py-4"> <footer className="footer fixed bottom-0 border-t border-base-300 bg-base-200 px-10 py-4 text-base-content">
<aside className="grid-flow-col items-center"> <aside className="grid-flow-col items-center">
<svg <svg
width="24" width="24"
@ -129,9 +168,9 @@ const ClientLayout = ({ children }: ClientLayoutProps) => {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fillRule="evenodd" fillRule="evenodd"
clipRule="evenodd" clipRule="evenodd"
className="fill-current"> className="fill-current"
<path >
d="M22.672 15.226l-2.432.811.841 2.515c.33 1.019-.209 2.127-1.23 2.456-1.15.325-2.148-.321-2.463-1.226l-.84-2.518-5.013 1.677.84 2.517c.391 1.203-.434 2.542-1.831 2.542-.88 0-1.601-.564-1.86-1.314l-.842-2.516-2.431.809c-1.135.328-2.145-.317-2.463-1.229-.329-1.018.211-2.127 1.231-2.456l2.432-.809-1.621-4.823-2.432.808c-1.355.384-2.558-.59-2.558-1.839 0-.817.509-1.582 1.327-1.846l2.433-.809-.842-2.515c-.33-1.02.211-2.129 1.232-2.458 1.02-.329 2.13.209 2.461 1.229l.842 2.515 5.011-1.677-.839-2.517c-.403-1.238.484-2.553 1.843-2.553.819 0 1.585.509 1.85 1.326l.841 2.517 2.431-.81c1.02-.33 2.131.211 2.461 1.229.332 1.018-.21 2.126-1.23 2.456l-2.433.809 1.622 4.823 2.433-.809c1.242-.401 2.557.484 2.557 1.838 0 .819-.51 1.583-1.328 1.847m-8.992-6.428l-5.01 1.675 1.619 4.828 5.011-1.674-1.62-4.829z"></path> <path d="M22.672 15.226l-2.432.811.841 2.515c.33 1.019-.209 2.127-1.23 2.456-1.15.325-2.148-.321-2.463-1.226l-.84-2.518-5.013 1.677.84 2.517c.391 1.203-.434 2.542-1.831 2.542-.88 0-1.601-.564-1.86-1.314l-.842-2.516-2.431.809c-1.135.328-2.145-.317-2.463-1.229-.329-1.018.211-2.127 1.231-2.456l2.432-.809-1.621-4.823-2.432.808c-1.355.384-2.558-.59-2.558-1.839 0-.817.509-1.582 1.327-1.846l2.433-.809-.842-2.515c-.33-1.02.211-2.129 1.232-2.458 1.02-.329 2.13.209 2.461 1.229l.842 2.515 5.011-1.677-.839-2.517c-.403-1.238.484-2.553 1.843-2.553.819 0 1.585.509 1.85 1.326l.841 2.517 2.431-.81c1.02-.33 2.131.211 2.461 1.229.332 1.018-.21 2.126-1.23 2.456l-2.433.809 1.622 4.823 2.433-.809c1.242-.401 2.557.484 2.557 1.838 0 .819-.51 1.583-1.328 1.847m-8.992-6.428l-5.01 1.675 1.619 4.828 5.011-1.674-1.62-4.829z"></path>
</svg> </svg>
<p> <p>
ACME Industries Ltd. ACME Industries Ltd.
@ -148,9 +187,9 @@ const ClientLayout = ({ children }: ClientLayoutProps) => {
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
className="fill-current"> className="fill-current"
<path >
d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"></path> <path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"></path>
</svg> </svg>
</a> </a>
<a> <a>
@ -159,9 +198,9 @@ const ClientLayout = ({ children }: ClientLayoutProps) => {
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
className="fill-current"> className="fill-current"
<path >
d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"></path> <path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"></path>
</svg> </svg>
</a> </a>
<a> <a>
@ -170,9 +209,9 @@ const ClientLayout = ({ children }: ClientLayoutProps) => {
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
className="fill-current"> className="fill-current"
<path >
d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z"></path> <path d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z"></path>
</svg> </svg>
</a> </a>
</div> </div>

View File

@ -0,0 +1,94 @@
import { useEffect, useRef } from 'react'
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
import { Metadata } 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<HTMLInputElement>(null)
useEffect(() => {
usernameRef?.current?.focus()
}, [])
const onSubmit = async (data: { username: string }) => {
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 (
<>
<Metadata 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: {
value: true,
message: 'Username is required',
},
}}
/>
<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

View File

@ -0,0 +1,133 @@
import { useEffect, useRef } from 'react'
import {
Form,
Label,
TextField,
PasswordField,
Submit,
FieldError,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
import { Metadata } 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<HTMLInputElement>(null)
useEffect(() => {
usernameRef.current?.focus()
}, [])
const onSubmit = async (data: Record<string, string>) => {
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 (
<>
<Metadata 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&apos;t have an account?</span>{' '}
<Link to={routes.signup()} className="rw-link">
Sign up!
</Link>
</div>
</div>
</main>
</>
)
}
export default LoginPage

View File

@ -0,0 +1,121 @@
import { useEffect, useRef, useState } from 'react'
import {
Form,
Label,
PasswordField,
Submit,
FieldError,
} from '@redwoodjs/forms'
import { navigate, routes } from '@redwoodjs/router'
import { Metadata } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
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()
}, [resetToken, validateResetToken])
const passwordRef = useRef<HTMLInputElement>(null)
useEffect(() => {
passwordRef.current?.focus()
}, [])
const onSubmit = async (data: Record<string, string>) => {
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 (
<>
<Metadata 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: 'New 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

View File

@ -0,0 +1,126 @@
import { useEffect, useRef } from 'react'
import {
Form,
Label,
TextField,
PasswordField,
FieldError,
Submit,
} from '@redwoodjs/forms'
import { Link, navigate, routes } from '@redwoodjs/router'
import { Metadata } 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<HTMLInputElement>(null)
useEffect(() => {
usernameRef.current?.focus()
}, [])
const onSubmit = async (data: Record<string, string>) => {
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 (
<>
<Metadata 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

View File

@ -4367,6 +4367,45 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@redwoodjs/auth-dbauth-api@npm:8.4.0":
version: 8.4.0
resolution: "@redwoodjs/auth-dbauth-api@npm:8.4.0"
dependencies:
"@redwoodjs/project-config": "npm:8.4.0"
base64url: "npm:3.0.1"
md5: "npm:2.3.0"
uuid: "npm:10.0.0"
checksum: 10c0/3984773e14117b8fc5907efce22bbdb512aab3174a96216525a0c40bf40d9c90e7dca20331c9c3447df2d5ce9b53d8b99d38e1b653367474101f88c1e58ac739
languageName: node
linkType: hard
"@redwoodjs/auth-dbauth-setup@npm:8.4.0":
version: 8.4.0
resolution: "@redwoodjs/auth-dbauth-setup@npm:8.4.0"
dependencies:
"@babel/runtime-corejs3": "npm:7.25.7"
"@prisma/internals": "npm:5.20.0"
"@redwoodjs/cli-helpers": "npm:8.4.0"
"@simplewebauthn/browser": "npm:7.4.0"
core-js: "npm:3.38.1"
prompts: "npm:2.4.2"
terminal-link: "npm:2.1.1"
checksum: 10c0/c2d4b528d55c503f3637f7587d7549c2ca0f0b9ab565657e566823db8cd4c04e03cb1395fe25b6edadf549db9d2222c3f913de4663d3a688b0786173214fa4cc
languageName: node
linkType: hard
"@redwoodjs/auth-dbauth-web@npm:8.4.0":
version: 8.4.0
resolution: "@redwoodjs/auth-dbauth-web@npm:8.4.0"
dependencies:
"@babel/runtime-corejs3": "npm:7.25.7"
"@redwoodjs/auth": "npm:8.4.0"
"@simplewebauthn/browser": "npm:7.4.0"
core-js: "npm:3.38.1"
checksum: 10c0/876fd9113e77809bb8c0fba0104f9f7b279694e283d963b4d061b12e965fef4855e43e8bcbcd3c599ad9ee187c89cb7c0c20c2c1b009afb75630b4fd8d52496c
languageName: node
linkType: hard
"@redwoodjs/auth@npm:8.4.0": "@redwoodjs/auth@npm:8.4.0":
version: 8.4.0 version: 8.4.0
resolution: "@redwoodjs/auth@npm:8.4.0" resolution: "@redwoodjs/auth@npm:8.4.0"
@ -5155,6 +5194,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@simplewebauthn/browser@npm:7.4.0":
version: 7.4.0
resolution: "@simplewebauthn/browser@npm:7.4.0"
dependencies:
"@simplewebauthn/typescript-types": "npm:^7.4.0"
checksum: 10c0/cd69d51511e1bb75603b254b706194e8b7c3849e8f02fcb373cc8bb8c789df803a1bb900de7853c0cc63c0ad81fd56497ca63885638d566137afa387674099ad
languageName: node
linkType: hard
"@simplewebauthn/typescript-types@npm:^7.4.0":
version: 7.4.0
resolution: "@simplewebauthn/typescript-types@npm:7.4.0"
checksum: 10c0/b7aefd742d2f483531ff96509475571339660addba1f140883d8e489601d6a3a5b1c6759aa5ba27a9da5b502709aee9f060a4d4e57010f32c94eb5c42ef562a3
languageName: node
linkType: hard
"@sinclair/typebox@npm:^0.27.8": "@sinclair/typebox@npm:^0.27.8":
version: 0.27.8 version: 0.27.8
resolution: "@sinclair/typebox@npm:0.27.8" resolution: "@sinclair/typebox@npm:0.27.8"
@ -6370,6 +6425,7 @@ __metadata:
resolution: "api@workspace:api" resolution: "api@workspace:api"
dependencies: dependencies:
"@redwoodjs/api": "npm:8.3.0" "@redwoodjs/api": "npm:8.3.0"
"@redwoodjs/auth-dbauth-api": "npm:8.4.0"
"@redwoodjs/graphql-server": "npm:8.3.0" "@redwoodjs/graphql-server": "npm:8.3.0"
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -6995,6 +7051,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"base64url@npm:3.0.1":
version: 3.0.1
resolution: "base64url@npm:3.0.1"
checksum: 10c0/5ca9d6064e9440a2a45749558dddd2549ca439a305793d4f14a900b7256b5f4438ef1b7a494e1addc66ced5d20f5c010716d353ed267e4b769e6c78074991241
languageName: node
linkType: hard
"binary-extensions@npm:^2.0.0": "binary-extensions@npm:^2.0.0":
version: 2.3.0 version: 2.3.0
resolution: "binary-extensions@npm:2.3.0" resolution: "binary-extensions@npm:2.3.0"
@ -7502,6 +7565,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"charenc@npm:0.0.2":
version: 0.0.2
resolution: "charenc@npm:0.0.2"
checksum: 10c0/a45ec39363a16799d0f9365c8dd0c78e711415113c6f14787a22462ef451f5013efae8a28f1c058f81fc01f2a6a16955f7a5fd0cd56247ce94a45349c89877d8
languageName: node
linkType: hard
"cheerio-select@npm:^2.1.0": "cheerio-select@npm:^2.1.0":
version: 2.1.0 version: 2.1.0
resolution: "cheerio-select@npm:2.1.0" resolution: "cheerio-select@npm:2.1.0"
@ -8141,6 +8211,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"crypt@npm:0.0.2":
version: 0.0.2
resolution: "crypt@npm:0.0.2"
checksum: 10c0/adbf263441dd801665d5425f044647533f39f4612544071b1471962209d235042fb703c27eea2795c7c53e1dfc242405173003f83cf4f4761a633d11f9653f18
languageName: node
linkType: hard
"crypto-browserify@npm:^3.11.0": "crypto-browserify@npm:^3.11.0":
version: 3.12.0 version: 3.12.0
resolution: "crypto-browserify@npm:3.12.0" resolution: "crypto-browserify@npm:3.12.0"
@ -11211,6 +11288,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-buffer@npm:~1.1.6":
version: 1.1.6
resolution: "is-buffer@npm:1.1.6"
checksum: 10c0/ae18aa0b6e113d6c490ad1db5e8df9bdb57758382b313f5a22c9c61084875c6396d50bbf49315f5b1926d142d74dfb8d31b40d993a383e0a158b15fea7a82234
languageName: node
linkType: hard
"is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7": "is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7":
version: 1.2.7 version: 1.2.7
resolution: "is-callable@npm:1.2.7" resolution: "is-callable@npm:1.2.7"
@ -13014,6 +13098,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"md5@npm:2.3.0":
version: 2.3.0
resolution: "md5@npm:2.3.0"
dependencies:
charenc: "npm:0.0.2"
crypt: "npm:0.0.2"
is-buffer: "npm:~1.1.6"
checksum: 10c0/14a21d597d92e5b738255fbe7fe379905b8cb97e0a49d44a20b58526a646ec5518c337b817ce0094ca94d3e81a3313879c4c7b510d250c282d53afbbdede9110
languageName: node
linkType: hard
"media-typer@npm:0.3.0": "media-typer@npm:0.3.0":
version: 0.3.0 version: 0.3.0
resolution: "media-typer@npm:0.3.0" resolution: "media-typer@npm:0.3.0"
@ -15483,6 +15578,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "root-workspace-0b6124@workspace:." resolution: "root-workspace-0b6124@workspace:."
dependencies: dependencies:
"@redwoodjs/auth-dbauth-setup": "npm:8.4.0"
"@redwoodjs/core": "npm:8.4.0" "@redwoodjs/core": "npm:8.4.0"
"@redwoodjs/project-config": "npm:8.4.0" "@redwoodjs/project-config": "npm:8.4.0"
prettier-plugin-tailwindcss: "npm:^0.6.8" prettier-plugin-tailwindcss: "npm:^0.6.8"
@ -17448,6 +17544,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "web@workspace:web" resolution: "web@workspace:web"
dependencies: dependencies:
"@redwoodjs/auth-dbauth-web": "npm:8.4.0"
"@redwoodjs/forms": "npm:8.4.0" "@redwoodjs/forms": "npm:8.4.0"
"@redwoodjs/router": "npm:8.4.0" "@redwoodjs/router": "npm:8.4.0"
"@redwoodjs/vite": "npm:8.4.0" "@redwoodjs/vite": "npm:8.4.0"