Redwood v0.37 updates
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@redwoodjs/api": "^0.36.0"
|
"@redwoodjs/api": "^0.37.0",
|
||||||
|
"@redwoodjs/graphql-server": "^0.37.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
23
api/src/directives/requireAuth/requireAuth.js
Normal file
23
api/src/directives/requireAuth/requireAuth.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
import { createValidatorDirective } from '@redwoodjs/graphql-server'
|
||||||
|
|
||||||
|
import { requireAuth as applicationRequireAuth } from 'src/lib/auth'
|
||||||
|
|
||||||
|
export const schema = gql`
|
||||||
|
"""
|
||||||
|
Use to check whether or not a user is authenticated and is associated
|
||||||
|
with an optional set of roles.
|
||||||
|
"""
|
||||||
|
directive @requireAuth(roles: [String]) on FIELD_DEFINITION
|
||||||
|
`
|
||||||
|
|
||||||
|
const validate = ({ directiveArgs }) => {
|
||||||
|
const { roles } = directiveArgs
|
||||||
|
|
||||||
|
applicationRequireAuth({ roles: roles })
|
||||||
|
}
|
||||||
|
|
||||||
|
const requireAuth = createValidatorDirective(schema, validate)
|
||||||
|
|
||||||
|
export default requireAuth
|
||||||
18
api/src/directives/requireAuth/requireAuth.test.js
Normal file
18
api/src/directives/requireAuth/requireAuth.test.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { mockRedwoodDirective, getDirectiveName } from '@redwoodjs/testing/api'
|
||||||
|
|
||||||
|
import requireAuth from './requireAuth'
|
||||||
|
|
||||||
|
describe('requireAuth directive', () => {
|
||||||
|
it('declares the directive sdl as schema, with the correct name', () => {
|
||||||
|
expect(requireAuth.schema).toBeTruthy()
|
||||||
|
expect(getDirectiveName(requireAuth.schema)).toBe('requireAuth')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('requireAuth has stub implementation. Should not throw when current user', () => {
|
||||||
|
// If you want to set values in context, pass it through e.g.
|
||||||
|
// mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }})
|
||||||
|
const mockExecution = mockRedwoodDirective(requireAuth, { context: {} })
|
||||||
|
|
||||||
|
expect(mockExecution).not.toThrowError()
|
||||||
|
})
|
||||||
|
})
|
||||||
16
api/src/directives/skipAuth/skipAuth.js
Normal file
16
api/src/directives/skipAuth/skipAuth.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
import { createValidatorDirective } from '@redwoodjs/graphql-server'
|
||||||
|
|
||||||
|
export const schema = gql`
|
||||||
|
"""
|
||||||
|
Use to skip authentication checks and allow public access.
|
||||||
|
"""
|
||||||
|
directive @skipAuth on FIELD_DEFINITION
|
||||||
|
`
|
||||||
|
|
||||||
|
const skipAuth = createValidatorDirective(schema, () => {
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
export default skipAuth
|
||||||
10
api/src/directives/skipAuth/skipAuth.test.js
Normal file
10
api/src/directives/skipAuth/skipAuth.test.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { getDirectiveName } from '@redwoodjs/testing/api'
|
||||||
|
|
||||||
|
import skipAuth from './skipAuth'
|
||||||
|
|
||||||
|
describe('skipAuth directive', () => {
|
||||||
|
it('declares the directive sdl as schema, with the correct name', () => {
|
||||||
|
expect(skipAuth.schema).toBeTruthy()
|
||||||
|
expect(getDirectiveName(skipAuth.schema)).toBe('skipAuth')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
import {
|
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
|
||||||
createGraphQLHandler,
|
|
||||||
makeMergedSchema,
|
|
||||||
makeServices,
|
|
||||||
} from '@redwoodjs/api'
|
|
||||||
|
|
||||||
import schemas from 'src/graphql/**/*.{js,ts}'
|
import directives from 'src/directives/**/*.{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 { getCurrentUser } from 'src/lib/auth'
|
import { getCurrentUser } from 'src/lib/auth'
|
||||||
@@ -14,10 +11,10 @@ import { logger } from 'src/lib/logger'
|
|||||||
export const handler = createGraphQLHandler({
|
export const handler = createGraphQLHandler({
|
||||||
loggerConfig: { logger, options: {} },
|
loggerConfig: { logger, options: {} },
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
schema: makeMergedSchema({
|
directives,
|
||||||
schemas,
|
sdls,
|
||||||
services: makeServices({ services }),
|
services,
|
||||||
}),
|
|
||||||
onException: () => {
|
onException: () => {
|
||||||
// Disconnect from your database with an unhandled exception.
|
// Disconnect from your database with an unhandled exception.
|
||||||
db.$disconnect()
|
db.$disconnect()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const schema = gql`
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
contacts: [Contact!]!
|
contacts: [Contact!]! @skipAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
input CreateContactInput {
|
input CreateContactInput {
|
||||||
@@ -24,6 +24,6 @@ export const schema = gql`
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
createContact(input: CreateContactInput!): Contact
|
createContact(input: CreateContactInput!): Contact @skipAuth
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export const schema = gql`
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
posts: [Post!]!
|
posts: [Post!]! @skipAuth
|
||||||
post(id: Int!): Post
|
post(id: Int!): Post @skipAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
input CreatePostInput {
|
input CreatePostInput {
|
||||||
@@ -22,8 +22,8 @@ export const schema = gql`
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
createPost(input: CreatePostInput!): Post!
|
createPost(input: CreatePostInput!): Post! @requireAuth
|
||||||
updatePost(id: Int!, input: UpdatePostInput!): Post!
|
updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth
|
||||||
deletePost(id: Int!): Post!
|
deletePost(id: Int!): Post! @requireAuth(roles: ["FOO"])
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -1,104 +1,78 @@
|
|||||||
// Define what you want `currentUser` to return throughout your app. For example,
|
import {
|
||||||
// to return a real user from your database, you could do something like:
|
AuthenticationError,
|
||||||
//
|
ForbiddenError,
|
||||||
// export const getCurrentUser = async ({ email }) => {
|
parseJWT,
|
||||||
// return await db.user.fineUnique({ where: { email } })
|
} from '@redwoodjs/graphql-server'
|
||||||
// }
|
import { logger } from 'src/lib/logger'
|
||||||
//
|
/**
|
||||||
// If you want to enforce role-based access ...
|
* getCurrentUser returns the user information together with
|
||||||
//
|
* an optional collection of roles used by requireAuth() to check
|
||||||
// You'll need to set the currentUser's roles attributes to the
|
* if the user is authenticated or has role-based access
|
||||||
// collection of roles as defined by your app.
|
*
|
||||||
//
|
* @param decoded - The decoded access token containing user info and JWT claims like `sub`
|
||||||
// This allows requireAuth() on the api side and hasRole() in the useAuth() hook on the web side
|
* @param { token, SupportedAuthTypes type } - The access token itself as well as the auth provider type
|
||||||
// to check if the user is assigned a given role or not.
|
* @param { APIGatewayEvent event, Context context } - An object which contains information from the invoker
|
||||||
//
|
* such as headers and cookies, and the context information about the invocation such as IP Address
|
||||||
// How you set the currentUser's roles depends on your auth provider and its implementation.
|
*
|
||||||
//
|
* @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
|
||||||
// For example, your decoded JWT may store `roles` in it namespaced `app_metadata`:
|
*/
|
||||||
//
|
export const getCurrentUser = async (
|
||||||
// {
|
decoded,
|
||||||
// 'https://example.com/app_metadata': { authorization: { roles: ['admin'] } },
|
{ _token, _type },
|
||||||
// 'https://example.com/user_metadata': {},
|
{ _event, _context }
|
||||||
// iss: 'https://app.us.auth0.com/',
|
) => {
|
||||||
// sub: 'email|1234',
|
if (!decoded) {
|
||||||
// aud: [
|
logger.warn('Missing decoded user')
|
||||||
// 'https://example.com',
|
return null
|
||||||
// 'https://app.us.auth0.com/userinfo'
|
}
|
||||||
// ],
|
|
||||||
// iat: 1596481520,
|
|
||||||
// exp: 1596567920,
|
|
||||||
// azp: '1l0w6JXXXXL880T',
|
|
||||||
// scope: 'openid profile email'
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// The parseJWT utility will extract the roles from decoded token.
|
|
||||||
//
|
|
||||||
// The app_medata claim may or may not be namespaced based on the auth provider.
|
|
||||||
// Note: Auth0 requires namespacing custom JWT claims
|
|
||||||
//
|
|
||||||
// Some providers, such as with Auth0, will set roles an authorization
|
|
||||||
// attribute in app_metadata (namespaced or not):
|
|
||||||
//
|
|
||||||
// 'app_metadata': { authorization: { roles: ['publisher'] } }
|
|
||||||
// 'https://example.com/app_metadata': { authorization: { roles: ['publisher'] } }
|
|
||||||
//
|
|
||||||
// Other providers may include roles simply within app_metadata:
|
|
||||||
//
|
|
||||||
// 'app_metadata': { roles: ['author'] }
|
|
||||||
// 'https://example.com/app_metadata': { roles: ['author'] }
|
|
||||||
//
|
|
||||||
// And yet other may define roles as a custom claim at the root of the decoded token:
|
|
||||||
//
|
|
||||||
// roles: ['admin']
|
|
||||||
//
|
|
||||||
// The function `getCurrentUser` should return the user information
|
|
||||||
// together with a collection of roles to check for role assignment:
|
|
||||||
|
|
||||||
import { AuthenticationError, ForbiddenError, parseJWT } from '@redwoodjs/api'
|
const { roles } = parseJWT({ decoded })
|
||||||
|
|
||||||
|
if (roles) {
|
||||||
|
return { ...decoded, roles }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...decoded }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use requireAuth in your services to check that a user is logged in,
|
* The user is authenticated if there is a currentUser in the context
|
||||||
* whether or not they are assigned a role, and optionally raise an
|
|
||||||
* error if they're not.
|
|
||||||
*
|
*
|
||||||
* @param {string=, string[]=} role - An optional role
|
* @returns {boolean} - If the currentUser is authenticated
|
||||||
*
|
|
||||||
* @example - No role-based access control.
|
|
||||||
*
|
|
||||||
* export const getCurrentUser = async (decoded) => {
|
|
||||||
* return await db.user.fineUnique({ where: { decoded.email } })
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @example - User info is conatined in the decoded token and roles extracted
|
|
||||||
*
|
|
||||||
* export const getCurrentUser = async (decoded, { _token, _type }) => {
|
|
||||||
* return { ...decoded, roles: parseJWT({ decoded }).roles }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @example - User record query by email with namespaced app_metadata roles
|
|
||||||
*
|
|
||||||
* export const getCurrentUser = async (decoded) => {
|
|
||||||
* const currentUser = await db.user.fineUnique({ where: { email: decoded.email } })
|
|
||||||
*
|
|
||||||
* return {
|
|
||||||
* ...currentUser,
|
|
||||||
* roles: parseJWT({ decoded: decoded, namespace: NAMESPACE }).roles,
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @example - User record query by an identity with app_metadata roles
|
|
||||||
*
|
|
||||||
* const getCurrentUser = async (decoded) => {
|
|
||||||
* const currentUser = await db.user.fineUnique({ where: { userIdentity: decoded.sub } })
|
|
||||||
* return {
|
|
||||||
* ...currentUser,
|
|
||||||
* roles: parseJWT({ decoded: decoded }).roles,
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*/
|
*/
|
||||||
export const getCurrentUser = async (decoded, { _token, _type }) => {
|
export const isAuthenticated = () => {
|
||||||
return { ...decoded, roles: parseJWT({ decoded }).roles }
|
return !!context.currentUser
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the currentUser is authenticated (and assigned one of the given roles)
|
||||||
|
*
|
||||||
|
* @param {string= | string[]=} roles - A single role or list of roles to check if the user belongs to
|
||||||
|
*
|
||||||
|
* @returns {boolean} - Returns true if the currentUser is authenticated (and assigned one of the given roles)
|
||||||
|
*/
|
||||||
|
export const hasRole = ({ roles }) => {
|
||||||
|
if (!isAuthenticated()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof roles !== 'undefined' &&
|
||||||
|
typeof roles === 'string' &&
|
||||||
|
context.currentUser.roles?.includes(roles)
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof roles !== 'undefined' &&
|
||||||
|
Array.isArray(roles) &&
|
||||||
|
context.currentUser.roles?.some((r) => roles.includes(r))
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,39 +80,21 @@ export const getCurrentUser = async (decoded, { _token, _type }) => {
|
|||||||
* whether or not they are assigned a role, and optionally raise an
|
* whether or not they are assigned a role, and optionally raise an
|
||||||
* error if they're not.
|
* error if they're not.
|
||||||
*
|
*
|
||||||
* @param {string=} roles - An optional role or list of roles
|
* @param {string= | string[]=} roles - A single role or list of roles to check if the user belongs to
|
||||||
* @param {string[]=} roles - An optional list of roles
|
|
||||||
|
|
||||||
* @example
|
|
||||||
*
|
*
|
||||||
* // checks if currentUser is authenticated
|
* @returns - If the currentUser is authenticated (and assigned one of the given roles)
|
||||||
* requireAuth()
|
|
||||||
*
|
*
|
||||||
* @example
|
* @throws {AuthenticationError} - If the currentUser is not authenticated
|
||||||
|
* @throws {ForbiddenError} If the currentUser is not allowed due to role permissions
|
||||||
*
|
*
|
||||||
* // checks if currentUser is authenticated and assigned one of the given roles
|
* @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
|
||||||
* requireAuth({ role: 'admin' })
|
|
||||||
* requireAuth({ role: ['editor', 'author'] })
|
|
||||||
* requireAuth({ role: ['publisher'] })
|
|
||||||
*/
|
*/
|
||||||
export const requireAuth = ({ role } = {}) => {
|
export const requireAuth = ({ roles } = {}) => {
|
||||||
if (!context.currentUser) {
|
if (!isAuthenticated) {
|
||||||
throw new AuthenticationError("You don't have permission to do that.")
|
throw new AuthenticationError("You don't have permission to do that.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!hasRole({ roles })) {
|
||||||
typeof role !== 'undefined' &&
|
|
||||||
typeof role === 'string' &&
|
|
||||||
!context.currentUser.roles?.includes(role)
|
|
||||||
) {
|
|
||||||
throw new ForbiddenError("You don't have access to do that.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof role !== 'undefined' &&
|
|
||||||
Array.isArray(role) &&
|
|
||||||
!context.currentUser.roles?.some((r) => role.includes(r))
|
|
||||||
) {
|
|
||||||
throw new ForbiddenError("You don't have access to do that.")
|
throw new ForbiddenError("You don't have access to do that.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { UserInputError } from '@redwoodjs/graphql-server'
|
||||||
import { db } from 'src/lib/db'
|
import { db } from 'src/lib/db'
|
||||||
import { UserInputError } from '@redwoodjs/api'
|
|
||||||
|
|
||||||
const validate = (input) => {
|
const validate = (input) => {
|
||||||
if (input.email && !input.email.match(/[^@]+@[^.]+\..+/)) {
|
if (input.email && !input.email.match(/[^@]+@[^.]+\..+/)) {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
export const standard = defineScenario({
|
export const standard = defineScenario({
|
||||||
contact: {
|
contact: {
|
||||||
john: {
|
john: {
|
||||||
name: 'John Doe',
|
data: {
|
||||||
email: 'john.doe@example.com',
|
name: 'John Doe',
|
||||||
message: 'I love RedwoodJS',
|
email: 'john.doe@example.com',
|
||||||
|
message: 'I love RedwoodJS',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { db } from 'src/lib/db'
|
import { db } from 'src/lib/db'
|
||||||
import { requireAuth } from 'src/lib/auth'
|
|
||||||
|
|
||||||
// Used when the environment variable REDWOOD_SECURE_SERVICES=1
|
|
||||||
export const beforeResolver = (rules) => {
|
|
||||||
rules.add(requireAuth)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const posts = () => {
|
export const posts = () => {
|
||||||
return db.post.findMany()
|
return db.post.findMany()
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
export const standard = defineScenario({
|
export const standard = defineScenario({
|
||||||
post: {
|
post: {
|
||||||
one: { title: 'String', body: 'String' },
|
one: {
|
||||||
two: { title: 'String', body: 'String' },
|
data: { title: 'String', body: 'String' }
|
||||||
|
},
|
||||||
|
two: {
|
||||||
|
data: { title: 'String', body: 'String' }
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redwoodjs/core": "^0.36.0"
|
"@redwoodjs/core": "^0.37.0"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "@redwoodjs/eslint-config"
|
"extends": "@redwoodjs/eslint-config"
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@redwoodjs/auth": "^0.36.0",
|
"@redwoodjs/auth": "^0.37.0",
|
||||||
"@redwoodjs/forms": "^0.36.0",
|
"@redwoodjs/forms": "^0.37.0",
|
||||||
"@redwoodjs/router": "^0.36.0",
|
"@redwoodjs/router": "^0.37.0",
|
||||||
"@redwoodjs/web": "^0.36.0",
|
"@redwoodjs/web": "^0.37.0",
|
||||||
"netlify-identity-widget": "^1.9.1",
|
"netlify-identity-widget": "^1.9.1",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
|||||||
@@ -34,77 +34,75 @@ const ContactPage = () => {
|
|||||||
console.log(data)
|
console.log(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <>
|
||||||
<>
|
<Toaster />
|
||||||
<Toaster />
|
<Form
|
||||||
<Form
|
onSubmit={onSubmit}
|
||||||
onSubmit={onSubmit}
|
config={{ mode: 'onBlur' }}
|
||||||
validation={{ mode: 'onBlur' }}
|
error={error}
|
||||||
|
formMethods={formMethods}
|
||||||
|
>
|
||||||
|
<FormError
|
||||||
error={error}
|
error={error}
|
||||||
formMethods={formMethods}
|
wrapperClassName="py-4 px-6 rounded-lg bg-red-100 text-red-700"
|
||||||
|
listClassName="list-disc ml-4"
|
||||||
|
listItemClassName=""
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
name="name"
|
||||||
|
className="block text-gray-700 uppercase text-sm"
|
||||||
|
errorClassName="block uppercase text-sm text-red-700"
|
||||||
>
|
>
|
||||||
<FormError
|
Name
|
||||||
error={error}
|
</Label>
|
||||||
wrapperClassName="py-4 px-6 rounded-lg bg-red-100 text-red-700"
|
<TextField
|
||||||
listClassName="list-disc ml-4"
|
name="name"
|
||||||
listItemClassName=""
|
validation={{ required: true }}
|
||||||
/>
|
className="border rounded-sm px-2 py-1 outline-none"
|
||||||
<Label
|
errorClassName="border rounded-sm px-2 py-1 border-red-700 outline-none"
|
||||||
name="name"
|
/>
|
||||||
className="block text-gray-700 uppercase text-sm"
|
<FieldError name="name" className="block text-red-700" />
|
||||||
errorClassName="block uppercase text-sm text-red-700"
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
</Label>
|
|
||||||
<TextField
|
|
||||||
name="name"
|
|
||||||
validation={{ required: true }}
|
|
||||||
className="border rounded-sm px-2 py-1 outline-none"
|
|
||||||
errorClassName="border rounded-sm px-2 py-1 border-red-700 outline-none"
|
|
||||||
/>
|
|
||||||
<FieldError name="name" className="block text-red-700" />
|
|
||||||
|
|
||||||
<Label
|
<Label
|
||||||
name="name"
|
name="name"
|
||||||
className="block mt-8 text-gray-700 uppercase text-sm"
|
className="block mt-8 text-gray-700 uppercase text-sm"
|
||||||
errorClassName="block mt-8 text-red-700 uppercase text-sm"
|
errorClassName="block mt-8 text-red-700 uppercase text-sm"
|
||||||
>
|
>
|
||||||
Email
|
Email
|
||||||
</Label>
|
</Label>
|
||||||
<TextField
|
<TextField
|
||||||
name="email"
|
name="email"
|
||||||
validation={{
|
validation={{
|
||||||
required: true,
|
required: true,
|
||||||
}}
|
}}
|
||||||
className="border rounded-sm px-2 py-1"
|
className="border rounded-sm px-2 py-1"
|
||||||
errorClassName="border rounded-sm px-2 py-1 border-red-700 outline-none"
|
errorClassName="border rounded-sm px-2 py-1 border-red-700 outline-none"
|
||||||
/>
|
/>
|
||||||
<FieldError name="email" className="block text-red-700" />
|
<FieldError name="email" className="block text-red-700" />
|
||||||
|
|
||||||
<Label
|
<Label
|
||||||
name="name"
|
name="name"
|
||||||
className="block mt-8 text-gray-700 uppercase text-sm"
|
className="block mt-8 text-gray-700 uppercase text-sm"
|
||||||
errorClassName="block mt-8 text-red-700 uppercase text-sm"
|
errorClassName="block mt-8 text-red-700 uppercase text-sm"
|
||||||
>
|
>
|
||||||
Message
|
Message
|
||||||
</Label>
|
</Label>
|
||||||
<TextAreaField
|
<TextAreaField
|
||||||
name="message"
|
name="message"
|
||||||
validation={{ required: true }}
|
validation={{ required: true }}
|
||||||
className="block border rounded-sm px-2 py-1"
|
className="block border rounded-sm px-2 py-1"
|
||||||
errorClassName="block border rounded-sm px-2 py-1 border-red-700 outline-none"
|
errorClassName="block border rounded-sm px-2 py-1 border-red-700 outline-none"
|
||||||
/>
|
/>
|
||||||
<FieldError name="message" className="block text-red-700" />
|
<FieldError name="message" className="block text-red-700" />
|
||||||
|
|
||||||
<Submit
|
<Submit
|
||||||
className="block bg-blue-700 text-white mt-8 px-4 py-2 rounded"
|
className="block bg-blue-700 text-white mt-8 px-4 py-2 rounded"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Submit>
|
</Submit>
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>;
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ContactPage
|
export default ContactPage
|
||||||
|
|||||||
Reference in New Issue
Block a user