Added registration page and functionality, improved error handling, fixed some critical security problems

This commit is contained in:
Dmitriy Shishkov 2020-10-20 00:35:08 +05:00
parent b8450525a5
commit 6c60520aae
No known key found for this signature in database
GPG Key ID: D76D70029F55183E
10 changed files with 176 additions and 73 deletions

View File

@ -99,4 +99,12 @@ const CREATEFORM = gql`
} }
` `
export { LOGIN, FORM, USER, FORMSUBMIT, CREATEFORM } const REGISTER = gql`
mutation Register($email: String!, $name: String!) {
register(email: $email, name: $name) {
success
}
}
`
export { LOGIN, FORM, USER, FORMSUBMIT, CREATEFORM, REGISTER }

View File

@ -1,11 +1,17 @@
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client' import {
ApolloClient,
createHttpLink,
from,
InMemoryCache,
} from '@apollo/client'
import { setContext } from '@apollo/client/link/context' import { setContext } from '@apollo/client/link/context'
import { onError } from '@apollo/client/link/error'
export * from './defs' export * from './defs'
const httpLink = createHttpLink({ const httpLink = createHttpLink({
uri: process.env.GRAPHQL_URL || process.env.REACT_APP_GRAPHQL_URL || undefined uri:
process.env.GRAPHQL_URL || process.env.REACT_APP_GRAPHQL_URL || undefined,
// fetchOptions: { // fetchOptions: {
// mode: 'no-cors' // mode: 'no-cors'
// } // }
@ -17,14 +23,25 @@ const authLink = setContext((_, { headers }) => {
return { return {
headers: { headers: {
...headers, ...headers,
authorization: token ? `Bearer ${token}` : '' authorization: token ? `Bearer ${token}` : '',
} },
} }
}) })
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.map(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
)
if (networkError) console.log(`[Network error]: ${networkError}`)
})
const client = new ApolloClient({ const client = new ApolloClient({
link: authLink.concat(httpLink), link: from([errorLink, authLink, httpLink]),
cache: new InMemoryCache() cache: new InMemoryCache(),
}) })
export default client export default client

View File

@ -9,6 +9,7 @@ import Authorize from './Authorize'
import CreateForm from './CreateForm' import CreateForm from './CreateForm'
import DoForm from './DoForm' import DoForm from './DoForm'
import Login from './Login' import Login from './Login'
import Register from './Register'
import UserPage from './UserPage' import UserPage from './UserPage'
const App: React.FC = () => { const App: React.FC = () => {
@ -25,6 +26,7 @@ const App: React.FC = () => {
<Route path="/user" component={UserPage} /> <Route path="/user" component={UserPage} />
<Route path="/form/:id" component={DoForm} /> <Route path="/form/:id" component={DoForm} />
<Route path="/create" component={CreateForm} /> <Route path="/create" component={CreateForm} />
<Route path="/register" component={Register} />
</Switch> </Switch>
</Router> </Router>
</Context.Provider> </Context.Provider>

View File

@ -57,13 +57,16 @@ type UseFormCreatorHookTurple<T extends string> = [
handleQuestionTitleChange: HandleQuestionTitleChangeType handleQuestionTitleChange: HandleQuestionTitleChangeType
handleAnswerVariantChange: HandleAnswerVariantChangeType handleAnswerVariantChange: HandleAnswerVariantChangeType
addVariant: AddVariantType addVariant: AddVariantType
} },
() => void
] ]
const initialState = { title: '', questions: [] }
export const useFormCreator = <T extends string>( export const useFormCreator = <T extends string>(
formatQuestionsToSubmit: FormatQuestionsToSubmitType formatQuestionsToSubmit: FormatQuestionsToSubmitType
): UseFormCreatorHookTurple<T> => { ): UseFormCreatorHookTurple<T> => {
const [form, setState] = useState<Form<T>>({ title: '', questions: [] }) const [form, setState] = useState<Form<T>>(initialState)
const [ const [
doFormCreation, doFormCreation,
@ -75,13 +78,11 @@ export const useFormCreator = <T extends string>(
}, },
}) })
const formSubmit: FormSubmitType = (e) => { const formSubmit: FormSubmitType = async (e) => {
e.preventDefault() e.preventDefault()
console.log({ try {
title: form.title, await doFormCreation()
questions: formatQuestionsToSubmit<T>(form.questions), } catch (err) {}
})
doFormCreation()
} }
const handleFormTitleChange: HandleFormTitleChangeType = (e) => { const handleFormTitleChange: HandleFormTitleChangeType = (e) => {
@ -92,7 +93,7 @@ export const useFormCreator = <T extends string>(
const createQuestion: CreateQuestionType<T> = (type) => { const createQuestion: CreateQuestionType<T> = (type) => {
setState(({ title, questions }) => ({ setState(({ title, questions }) => ({
title, title,
questions: questions.concat({ title: '', type, variants: [] }), questions: questions.concat({ title: '', type, variants: [''] }),
})) }))
} }
@ -131,7 +132,6 @@ export const useFormCreator = <T extends string>(
} }
const addVariant: AddVariantType = (questionNumber) => { const addVariant: AddVariantType = (questionNumber) => {
console.log()
setState(({ title, questions }) => ({ setState(({ title, questions }) => ({
title, title,
questions: questions.map((el, index) => ({ questions: questions.map((el, index) => ({
@ -142,6 +142,10 @@ export const useFormCreator = <T extends string>(
})) }))
} }
const resetForm = () => {
setState(initialState)
}
return [ return [
form, form,
[formSubmit, { submitData, submitError, submitLoading }], [formSubmit, { submitData, submitError, submitLoading }],
@ -152,5 +156,6 @@ export const useFormCreator = <T extends string>(
handleAnswerVariantChange, handleAnswerVariantChange,
addVariant, addVariant,
}, },
resetForm,
] ]
} }

View File

@ -37,11 +37,17 @@ const CreateForm: React.FC = () => {
handleAnswerVariantChange, handleAnswerVariantChange,
addVariant, addVariant,
}, },
resetForm,
] = useFormCreator<QuestionTypes>(formatQuestionsToSubmit) ] = useFormCreator<QuestionTypes>(formatQuestionsToSubmit)
return ( return (
<> <>
<form onSubmit={formSubmit}> <form
onSubmit={(e) => {
resetForm()
formSubmit(e)
}}
>
<label> <label>
Title: Title:
<input <input
@ -49,6 +55,7 @@ const CreateForm: React.FC = () => {
name="Title" name="Title"
value={form.title} value={form.title}
onChange={handleFormTitleChange} onChange={handleFormTitleChange}
required
/> />
</label> </label>
@ -75,6 +82,7 @@ const CreateForm: React.FC = () => {
<li key={questionIndex}> <li key={questionIndex}>
<p>{quesstion.type} question:</p> <p>{quesstion.type} question:</p>
<input <input
required
type="text" type="text"
name="questionTitle" name="questionTitle"
placeholder="Title" placeholder="Title"
@ -89,6 +97,7 @@ const CreateForm: React.FC = () => {
{quesstion.variants.map((variant, variantIndex) => ( {quesstion.variants.map((variant, variantIndex) => (
<li key={variantIndex}> <li key={variantIndex}>
<input <input
required
placeholder="Variant" placeholder="Variant"
type="text" type="text"
value={variant} value={variant}

View File

@ -2,8 +2,6 @@ import { useState } from 'react'
import { ChoiseAnswer, InputAnswer } from '../../apollo/typeDefs.gen' import { ChoiseAnswer, InputAnswer } from '../../apollo/typeDefs.gen'
export const useForm = (initialValue?: (InputAnswer | ChoiseAnswer)[]) => { export const useForm = (initialValue?: (InputAnswer | ChoiseAnswer)[]) => {
console.log(initialValue, 'Inside hook')
const [answers, setAnswer] = useState<(InputAnswer | ChoiseAnswer)[]>( const [answers, setAnswer] = useState<(InputAnswer | ChoiseAnswer)[]>(
initialValue || [] initialValue || []
) )

View File

@ -8,7 +8,9 @@ import {
Form, Form,
InputAnswer, InputAnswer,
InputQuestion, InputQuestion,
MutationFormSubmitArgs,
QueryFormArgs, QueryFormArgs,
ServerAnswer,
} from '../../apollo/typeDefs.gen' } from '../../apollo/typeDefs.gen'
import Lists from './Lists' import Lists from './Lists'
@ -16,12 +18,19 @@ interface IFormQuery {
form: Form form: Form
} }
interface IFormSubmitMutation {
formSubmit: ServerAnswer
}
const DoForm: React.FC = () => { const DoForm: React.FC = () => {
const { id: idString } = useParams<{ id: string }>() const { id: idString } = useParams<{ id: string }>()
const id = parseInt(idString) const id = parseInt(idString)
const { data, error, loading } = useQuery<IFormQuery, QueryFormArgs>(FORM, { const { data, error, loading, refetch: refetchForm } = useQuery<
IFormQuery,
QueryFormArgs
>(FORM, {
variables: { id }, variables: { id },
skip: isNaN(id), skip: isNaN(id),
}) })
@ -29,7 +38,7 @@ const DoForm: React.FC = () => {
const [ const [
doFormSubmit, doFormSubmit,
{ error: submitError, data: submitData, loading: submitLoading }, { error: submitError, data: submitData, loading: submitLoading },
] = useMutation(FORMSUBMIT) ] = useMutation<IFormSubmitMutation, MutationFormSubmitArgs>(FORMSUBMIT)
const [answers, setAnswer] = useState<(InputAnswer | ChoiseAnswer)[]>([]) const [answers, setAnswer] = useState<(InputAnswer | ChoiseAnswer)[]>([])
@ -57,8 +66,6 @@ const DoForm: React.FC = () => {
} }
}, [data]) }, [data])
useEffect(() => console.log(answers), [answers])
if (isNaN(id)) return <Redirect to="/" /> if (isNaN(id)) return <Redirect to="/" />
if (loading) return <div>Loading...</div> if (loading) return <div>Loading...</div>
@ -66,24 +73,24 @@ const DoForm: React.FC = () => {
const { form } = data! const { form } = data!
const handleSubmit = (e: FormEvent) => { const handleSubmit = async (e: FormEvent) => {
e.preventDefault() e.preventDefault()
console.log('Submited form:', answers)
answers.forEach((el) => { answers.forEach((el) => {
delete el.__typename delete el.__typename
}) })
const submitAnswers = JSON.stringify(answers) try {
const submitAnswers = JSON.stringify(answers)
console.log('Filtered answers: ', submitAnswers) await doFormSubmit({
variables: {
doFormSubmit({ formId: id,
variables: { answers: submitAnswers,
formId: id, },
answers: submitAnswers, })
}, await refetchForm()
}) } catch (err) {}
} }
const answerChange = (num: number) => { const answerChange = (num: number) => {
@ -108,39 +115,43 @@ const DoForm: React.FC = () => {
<p>{form.dateCreated}</p> <p>{form.dateCreated}</p>
<h3>{form.author?.name || 'No author'}</h3> <h3>{form.author?.name || 'No author'}</h3>
{form.submissions ? ( {form.submissions ? (
<div> form.submissions.length > 0 ? (
<h1>Submission{form.submissions.length > 1 && 's'}:</h1> <div>
<ul> <h1>Submission{form.submissions.length > 1 && 's'}:</h1>
{form.submissions.map((submission, submissionIndex) => ( <ul>
<li key={submissionIndex}> {form.submissions.map((submission, submissionIndex) => (
<h2> <li key={submissionIndex}>
User:{' '} <h2>
{submission.user ? submission.user.name : 'No submitter'} User:{' '}
</h2> {submission.user ? submission.user.name : 'No submitter'}
<ul> </h2>
{submission.answers.map( <ul>
(answer: InputAnswer | ChoiseAnswer, answerIndex) => ( {submission.answers.map(
<li key={answerIndex}> (answer: InputAnswer | ChoiseAnswer, answerIndex) => (
<h3>{form.questions[answerIndex].title}</h3> <li key={answerIndex}>
{answer.__typename === 'ChoiseAnswer' && ( <h3>{form.questions[answerIndex].title}</h3>
<h4> {answer.__typename === 'ChoiseAnswer' && (
{ <h4>
(form.questions[answerIndex] as ChoisesQuestion) {
.variants[answer.userChoise].text (form.questions[answerIndex] as ChoisesQuestion)
} .variants[answer.userChoise].text
</h4> }
)} </h4>
{answer.__typename === 'InputAnswer' && ( )}
<h4>{answer.userInput}</h4> {answer.__typename === 'InputAnswer' && (
)} <h4>{answer.userInput}</h4>
</li> )}
) </li>
)} )
</ul> )}
</li> </ul>
))} </li>
</ul> ))}
</div> </ul>
</div>
) : (
'No submissions yet'
)
) : ( ) : (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<ul> <ul>

View File

@ -3,11 +3,19 @@ import React, { ChangeEvent, FormEvent, useState } from 'react'
import { Redirect } from 'react-router-dom' import { Redirect } from 'react-router-dom'
import { LOGIN } from '../../apollo' import { LOGIN } from '../../apollo'
import { MutationLoginArgs, ServerAnswer } from '../../apollo/typeDefs.gen'
interface ILoginMutation {
login: ServerAnswer
}
const Login: React.FC = () => { const Login: React.FC = () => {
const [email, setEmail] = useState<string>('') const [email, setEmail] = useState<string>('')
const [doLogin, { error, data }] = useMutation(LOGIN) const [doLogin, { error, data }] = useMutation<
ILoginMutation,
MutationLoginArgs
>(LOGIN)
const handleFormSubmit = async (e: FormEvent) => { const handleFormSubmit = async (e: FormEvent) => {
e.preventDefault() e.preventDefault()

View File

@ -0,0 +1,45 @@
import { useMutation } from '@apollo/client'
import React, { FormEvent } from 'react'
import { Redirect } from 'react-router-dom'
import { REGISTER } from '../../apollo'
import { MutationRegisterArgs, ServerAnswer } from '../../apollo/typeDefs.gen'
interface IRegisterMutation {
register: ServerAnswer
}
const Register: React.FC = () => {
const [doRegister, { data, loading, error }] = useMutation<
IRegisterMutation,
MutationRegisterArgs
>(REGISTER)
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
try {
await doRegister({
variables: {
email: formData.get('email') as string,
name: formData.get('name') as string,
},
})
} catch (err) {}
}
return (
<div>
Register
<form onSubmit={handleSubmit}>
<input type="email" name="email" placeholder="email" />
<input type="text" name="name" placeholder="username" />
{loading ? 'Loading...' : <input type="submit" value="Submit" />}
{error && error.message}
{data && data.register && data.register.success && <Redirect to="/" />}
</form>
</div>
)
}
export default Register

View File

@ -1,14 +1,14 @@
import { useQuery } from '@apollo/client' import { useQuery } from '@apollo/client'
import React from 'react' import React from 'react'
import { USER } from '../../apollo' import { USER } from '../../apollo'
import { User } from '../../apollo/typeDefs.gen' import { QueryUserArgs, User } from '../../apollo/typeDefs.gen'
interface UserQuery { interface IUserQuery {
user: User user: User
} }
const UserPage: React.FC = () => { const UserPage: React.FC = () => {
const { data, error, loading } = useQuery<UserQuery>(USER) const { data, error, loading } = useQuery<IUserQuery, QueryUserArgs>(USER)
if (loading) return <p>Loading...</p> if (loading) return <p>Loading...</p>
if (error) return <p>{error.message}</p> if (error) return <p>{error.message}</p>