New: Username and Password authentication
This commit is contained in:
parent
7c21d11e8d
commit
77974e2c61
|
@ -0,0 +1,24 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UserExample" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"name" TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"hashedPassword" TEXT NOT NULL,
|
||||||
|
"salt" TEXT NOT NULL,
|
||||||
|
"resetToken" TEXT,
|
||||||
|
"resetTokenExpiresAt" DATETIME,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UserExample_email_key" ON "UserExample"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
3
api/db/migrations/migration_lock.toml
Normal file
3
api/db/migrations/migration_lock.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "sqlite"
|
|
@ -22,3 +22,16 @@ model UserExample {
|
||||||
email String @unique
|
email String @unique
|
||||||
name String?
|
name String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
email String @unique
|
||||||
|
firstName String?
|
||||||
|
lastName String?
|
||||||
|
hashedPassword String?
|
||||||
|
salt String?
|
||||||
|
resetToken String?
|
||||||
|
resetTokenExpiresAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
|
@ -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.3.0",
|
||||||
"@redwoodjs/graphql-server": "8.3.0"
|
"@redwoodjs/graphql-server": "8.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
207
api/src/functions/auth.ts
Normal file
207
api/src/functions/auth.ts
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
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,
|
||||||
|
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()
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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 !== 'number') {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCurrentUser = async () => {
|
/**
|
||||||
throw new Error(
|
* Use requireAuth in your services to check that a user is logged in,
|
||||||
'Auth is not set up yet. See https://redwoodjs.com/docs/authentication ' +
|
* whether or not they are assigned a role, and optionally raise an
|
||||||
'to get started'
|
* 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.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@redwoodjs/auth-dbauth-setup": "8.3.0",
|
||||||
"@redwoodjs/core": "8.3.0",
|
"@redwoodjs/core": "8.3.0",
|
||||||
"@redwoodjs/project-config": "8.3.0"
|
"@redwoodjs/project-config": "8.3.0"
|
||||||
},
|
},
|
||||||
|
@ -23,5 +24,8 @@
|
||||||
"packageManager": "yarn@4.4.0",
|
"packageManager": "yarn@4.4.0",
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@storybook/react-dom-shim@npm:7.6.17": "https://verdaccio.tobbe.dev/@storybook/react-dom-shim/-/react-dom-shim-8.0.8.tgz"
|
"@storybook/react-dom-shim@npm:7.6.17": "https://verdaccio.tobbe.dev/@storybook/react-dom-shim/-/react-dom-shim-8.0.8.tgz"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"crypto-js": "^4.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@redwoodjs/auth-dbauth-web": "8.3.0",
|
||||||
"@redwoodjs/forms": "8.3.0",
|
"@redwoodjs/forms": "8.3.0",
|
||||||
"@redwoodjs/router": "8.3.0",
|
"@redwoodjs/router": "8.3.0",
|
||||||
"@redwoodjs/web": "8.3.0",
|
"@redwoodjs/web": "8.3.0",
|
||||||
|
|
|
@ -5,7 +5,11 @@ 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'
|
||||||
|
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
|
@ -14,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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,8 +9,15 @@
|
||||||
|
|
||||||
import { Router, Route } from '@redwoodjs/router'
|
import { Router, Route } from '@redwoodjs/router'
|
||||||
|
|
||||||
|
import { useAuth } from './auth'
|
||||||
|
|
||||||
const Routes = () => {
|
const Routes = () => {
|
||||||
return (
|
return (
|
||||||
|
<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" />
|
||||||
<Route path="/" page={HomePage} name="home" />
|
<Route path="/" page={HomePage} name="home" />
|
||||||
<Route notfound page={NotFoundPage} />
|
<Route notfound page={NotFoundPage} />
|
||||||
</Router>
|
</Router>
|
||||||
|
|
5
web/src/auth.ts
Normal file
5
web/src/auth.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { createDbAuthClient, createAuth } from '@redwoodjs/auth-dbauth-web'
|
||||||
|
|
||||||
|
const dbAuthClient = createDbAuthClient()
|
||||||
|
|
||||||
|
export const { AuthProvider, useAuth } = createAuth(dbAuthClient)
|
94
web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
Normal file
94
web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
Normal 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
|
143
web/src/pages/LoginPage/LoginPage.tsx
Normal file
143
web/src/pages/LoginPage/LoginPage.tsx
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
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't have an account?</span>{' '}
|
||||||
|
<Link to={routes.signup()} className="rw-link">
|
||||||
|
Sign up!
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={`https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${
|
||||||
|
process.env.MICROSOFT_OAUTH_CLIENT_ID
|
||||||
|
}&grant_type=authorization_code&response_type=code&redirect_uri=${
|
||||||
|
process.env.MICROSOFT_OAUTH_REDIRECT_URI
|
||||||
|
}&scope=${process.env.MICROSOFT_OAUTH_SCOPES.split(' ').join('+')}`}
|
||||||
|
className="mx-auto block w-48 rounded bg-gray-800 px-4 py-2 text-center text-xs font-semibold uppercase tracking-wide text-white"
|
||||||
|
>
|
||||||
|
Login with GitHub
|
||||||
|
</a>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginPage
|
121
web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
Normal file
121
web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
Normal 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
|
126
web/src/pages/SignupPage/SignupPage.tsx
Normal file
126
web/src/pages/SignupPage/SignupPage.tsx
Normal 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
|
397
web/src/scaffold.css
Normal file
397
web/src/scaffold.css
Normal file
|
@ -0,0 +1,397 @@
|
||||||
|
/*
|
||||||
|
normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css
|
||||||
|
*/
|
||||||
|
|
||||||
|
.rw-scaffold *,
|
||||||
|
.rw-scaffold ::after,
|
||||||
|
.rw-scaffold ::before {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
.rw-scaffold main {
|
||||||
|
color: #4a5568;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.rw-scaffold h1,
|
||||||
|
.rw-scaffold h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.rw-scaffold a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.rw-scaffold ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.rw-scaffold input {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.rw-scaffold input:-ms-input-placeholder {
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
.rw-scaffold input::-ms-input-placeholder {
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
.rw-scaffold input::placeholder {
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
.rw-scaffold table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Style
|
||||||
|
*/
|
||||||
|
|
||||||
|
.rw-scaffold,
|
||||||
|
.rw-toast {
|
||||||
|
background-color: #fff;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
|
||||||
|
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
|
}
|
||||||
|
.rw-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem 2rem 1rem 2rem;
|
||||||
|
}
|
||||||
|
.rw-main {
|
||||||
|
margin-left: 1rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.rw-segment {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
scrollbar-color: #a1a1aa transparent;
|
||||||
|
}
|
||||||
|
.rw-segment::-webkit-scrollbar {
|
||||||
|
height: initial;
|
||||||
|
}
|
||||||
|
.rw-segment::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
border-width: 1px 0 0 0;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
.rw-segment::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #a1a1aa;
|
||||||
|
background-clip: content-box;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.rw-segment-header {
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
color: #4a5568;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
.rw-segment-main {
|
||||||
|
background-color: #f7fafc;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.rw-link {
|
||||||
|
color: #4299e1;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.rw-link:hover {
|
||||||
|
color: #2b6cb0;
|
||||||
|
}
|
||||||
|
.rw-forgot-link {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #a0aec0;
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
}
|
||||||
|
.rw-forgot-link:hover {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #4299e1;
|
||||||
|
}
|
||||||
|
.rw-heading {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.rw-heading.rw-heading-primary {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
.rw-heading.rw-heading-secondary {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.rw-heading .rw-link {
|
||||||
|
color: #4a5568;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.rw-heading .rw-link:hover {
|
||||||
|
color: #1a202c;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.rw-cell-error {
|
||||||
|
font-size: 90%;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.rw-form-wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: -1rem;
|
||||||
|
}
|
||||||
|
.rw-cell-error,
|
||||||
|
.rw-form-error-wrapper {
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #fff5f5;
|
||||||
|
color: #c53030;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: #feb2b2;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.rw-form-error-title {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.rw-form-error-list {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
list-style-type: disc;
|
||||||
|
list-style-position: inside;
|
||||||
|
}
|
||||||
|
.rw-button {
|
||||||
|
border: none;
|
||||||
|
color: #718096;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.25rem 1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-decoration: none;
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
line-height: 2;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
.rw-button:hover {
|
||||||
|
background-color: #718096;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.rw-button.rw-button-small {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border-radius: 0.125rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
.rw-button.rw-button-green {
|
||||||
|
background-color: #48bb78;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.rw-button.rw-button-green:hover {
|
||||||
|
background-color: #38a169;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.rw-button.rw-button-blue {
|
||||||
|
background-color: #3182ce;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.rw-button.rw-button-blue:hover {
|
||||||
|
background-color: #2b6cb0;
|
||||||
|
}
|
||||||
|
.rw-button.rw-button-red {
|
||||||
|
background-color: #e53e3e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.rw-button.rw-button-red:hover {
|
||||||
|
background-color: #c53030;
|
||||||
|
}
|
||||||
|
.rw-button-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
.rw-button-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0.75rem 0.5rem;
|
||||||
|
}
|
||||||
|
.rw-button-group .rw-button {
|
||||||
|
margin: 0 0.25rem;
|
||||||
|
}
|
||||||
|
.rw-form-wrapper .rw-button-group {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.rw-label {
|
||||||
|
display: block;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
color: #4a5568;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.rw-label.rw-label-error {
|
||||||
|
color: #c53030;
|
||||||
|
}
|
||||||
|
.rw-input {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
color: #4a5568;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.rw-check-radio-item-none {
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
.rw-check-radio-items {
|
||||||
|
display: flex;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
.rw-input[type='checkbox'] {
|
||||||
|
display: inline;
|
||||||
|
width: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
.rw-input[type='radio'] {
|
||||||
|
display: inline;
|
||||||
|
width: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
.rw-input:focus {
|
||||||
|
border-color: #a0aec0;
|
||||||
|
}
|
||||||
|
.rw-input-error {
|
||||||
|
border-color: #c53030;
|
||||||
|
color: #c53030;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rw-input-error:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #c53030;
|
||||||
|
box-shadow: 0 0 5px #c53030;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rw-field-error {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #c53030;
|
||||||
|
}
|
||||||
|
.rw-table-wrapper-responsive {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.rw-table-wrapper-responsive .rw-table {
|
||||||
|
min-width: 48rem;
|
||||||
|
}
|
||||||
|
.rw-table {
|
||||||
|
table-layout: auto;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.rw-table th,
|
||||||
|
.rw-table td {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
.rw-table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
.rw-table tr:nth-child(odd) td,
|
||||||
|
.rw-table tr:nth-child(odd) th {
|
||||||
|
background-color: #f7fafc;
|
||||||
|
}
|
||||||
|
.rw-table thead tr {
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
.rw-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.rw-table thead th {
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.rw-table tbody th {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.rw-table tbody th {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rw-table tbody tr {
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
.rw-table input {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.rw-table-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
height: 17px;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
.rw-table-actions .rw-button {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.rw-table-actions .rw-button:hover {
|
||||||
|
background-color: #718096;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.rw-table-actions .rw-button-blue {
|
||||||
|
color: #3182ce;
|
||||||
|
}
|
||||||
|
.rw-table-actions .rw-button-blue:hover {
|
||||||
|
background-color: #3182ce;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.rw-table-actions .rw-button-red {
|
||||||
|
color: #e53e3e;
|
||||||
|
}
|
||||||
|
.rw-table-actions .rw-button-red:hover {
|
||||||
|
background-color: #e53e3e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.rw-text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.rw-login-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24rem;
|
||||||
|
margin: 4rem auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.rw-login-container .rw-form-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.rw-login-link {
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 90%;
|
||||||
|
text-align: center;
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
.rw-webauthn-wrapper {
|
||||||
|
margin: 1.5rem 1rem 1rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.rw-webauthn-wrapper h2 {
|
||||||
|
font-size: 150%;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
109
yarn.lock
109
yarn.lock
|
@ -4342,6 +4342,45 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@redwoodjs/auth-dbauth-api@npm:8.3.0":
|
||||||
|
version: 8.3.0
|
||||||
|
resolution: "@redwoodjs/auth-dbauth-api@npm:8.3.0"
|
||||||
|
dependencies:
|
||||||
|
"@redwoodjs/project-config": "npm:8.3.0"
|
||||||
|
base64url: "npm:3.0.1"
|
||||||
|
md5: "npm:2.3.0"
|
||||||
|
uuid: "npm:10.0.0"
|
||||||
|
checksum: 10c0/35ed29c19cad11e65cc43ed77c37055ece043895c06a82e491db123b45e40d9710c8ed114b86384647b0ade658a03289b7346a9fa0f3d07ca4157104d134b35f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@redwoodjs/auth-dbauth-setup@npm:8.3.0":
|
||||||
|
version: 8.3.0
|
||||||
|
resolution: "@redwoodjs/auth-dbauth-setup@npm:8.3.0"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime-corejs3": "npm:7.25.6"
|
||||||
|
"@prisma/internals": "npm:5.20.0"
|
||||||
|
"@redwoodjs/cli-helpers": "npm:8.3.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/6a5df110e0db68a2509d03e959492687bd42c0c293f6c56986285f1f8fc5121bc92a04805f340b524ac39f620f8befa5ea9db5c8938023483c79402273fd1c21
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@redwoodjs/auth-dbauth-web@npm:8.3.0":
|
||||||
|
version: 8.3.0
|
||||||
|
resolution: "@redwoodjs/auth-dbauth-web@npm:8.3.0"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime-corejs3": "npm:7.25.6"
|
||||||
|
"@redwoodjs/auth": "npm:8.3.0"
|
||||||
|
"@simplewebauthn/browser": "npm:7.4.0"
|
||||||
|
core-js: "npm:3.38.1"
|
||||||
|
checksum: 10c0/8d06aca942a0a2381454eac2c2850f4500afe46477870dd6f8cbcf8026a0b64f54d4788053806e2392ebc4e29eb27f3e0820db648c49e2220fa183ca8fe06bb3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@redwoodjs/auth@npm:8.3.0":
|
"@redwoodjs/auth@npm:8.3.0":
|
||||||
version: 8.3.0
|
version: 8.3.0
|
||||||
resolution: "@redwoodjs/auth@npm:8.3.0"
|
resolution: "@redwoodjs/auth@npm:8.3.0"
|
||||||
|
@ -5096,6 +5135,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"
|
||||||
|
@ -6311,6 +6366,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.3.0"
|
||||||
"@redwoodjs/graphql-server": "npm:8.3.0"
|
"@redwoodjs/graphql-server": "npm:8.3.0"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
@ -6918,6 +6974,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"
|
||||||
|
@ -7417,6 +7480,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"
|
||||||
|
@ -8025,6 +8095,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"
|
||||||
|
@ -8044,6 +8121,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"crypto-js@npm:^4.2.0":
|
||||||
|
version: 4.2.0
|
||||||
|
resolution: "crypto-js@npm:4.2.0"
|
||||||
|
checksum: 10c0/8fbdf9d56f47aea0794ab87b0eb9833baf80b01a7c5c1b0edc7faf25f662fb69ab18dc2199e2afcac54670ff0cd9607a9045a3f7a80336cccd18d77a55b9fdf0
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"crypto-random-string@npm:^1.0.0":
|
"crypto-random-string@npm:^1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "crypto-random-string@npm:1.0.0"
|
resolution: "crypto-random-string@npm:1.0.0"
|
||||||
|
@ -11029,6 +11113,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"
|
||||||
|
@ -12834,6 +12925,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"
|
||||||
|
@ -15107,8 +15209,10 @@ __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.3.0"
|
||||||
"@redwoodjs/core": "npm:8.3.0"
|
"@redwoodjs/core": "npm:8.3.0"
|
||||||
"@redwoodjs/project-config": "npm:8.3.0"
|
"@redwoodjs/project-config": "npm:8.3.0"
|
||||||
|
crypto-js: "npm:^4.2.0"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
@ -16521,11 +16625,11 @@ __metadata:
|
||||||
|
|
||||||
"typescript@patch:typescript@npm%3A5.6.2#optional!builtin<compat/typescript>":
|
"typescript@patch:typescript@npm%3A5.6.2#optional!builtin<compat/typescript>":
|
||||||
version: 5.6.2
|
version: 5.6.2
|
||||||
resolution: "typescript@patch:typescript@npm%3A5.6.2#optional!builtin<compat/typescript>::version=5.6.2&hash=74658d"
|
resolution: "typescript@patch:typescript@npm%3A5.6.2#optional!builtin<compat/typescript>::version=5.6.2&hash=8c6c40"
|
||||||
bin:
|
bin:
|
||||||
tsc: bin/tsc
|
tsc: bin/tsc
|
||||||
tsserver: bin/tsserver
|
tsserver: bin/tsserver
|
||||||
checksum: 10c0/e6c1662e4852e22fe4bbdca471dca3e3edc74f6f1df043135c44a18a7902037023ccb0abdfb754595ca9028df8920f2f8492c00fc3cbb4309079aae8b7de71cd
|
checksum: 10c0/94eb47e130d3edd964b76da85975601dcb3604b0c848a36f63ac448d0104e93819d94c8bdf6b07c00120f2ce9c05256b8b6092d23cf5cf1c6fa911159e4d572f
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -16995,6 +17099,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.3.0"
|
||||||
"@redwoodjs/forms": "npm:8.3.0"
|
"@redwoodjs/forms": "npm:8.3.0"
|
||||||
"@redwoodjs/router": "npm:8.3.0"
|
"@redwoodjs/router": "npm:8.3.0"
|
||||||
"@redwoodjs/vite": "npm:8.3.0"
|
"@redwoodjs/vite": "npm:8.3.0"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user