Connected signing up and signing in to back

This commit is contained in:
Dmitriy Shishkov 2023-07-29 10:41:54 +03:00
parent 0e5aeae491
commit 85472233a3
Signed by: dm1sh
GPG Key ID: 027994B0AA357688
14 changed files with 228 additions and 168 deletions

View File

@ -0,0 +1,24 @@
import { API_URL } from '../../config'
import { fallbackTo, isString } from '../../utils/types'
import { SignUp, SignUpBody, SignUpResponse } from './types'
const composeSignUpURL = () => (
API_URL + '/signup?'
)
const composeSignUpBody = (formData: FormData): SignUpBody => ({
email: fallbackTo(formData.get('email'), isString, ''),
password: fallbackTo(formData.get('password'), isString, ''),
name: fallbackTo(formData.get('name'), isString, ''),
surname: fallbackTo(formData.get('surname'), isString, ''),
})
const processSignUp = (data: SignUpResponse): SignUp => {
if (!data.Success) {
throw new Error(data.Message)
}
return true
}
export { composeSignUpURL, composeSignUpBody, processSignUp }

View File

@ -0,0 +1,30 @@
import { isConst, isObject } from '../../utils/types'
type SignUpBody = {
email: string,
password: string,
name: string,
surname: string,
}
type SignUpResponse = {
Success: true,
} | {
Success: false,
Message: string
}
const isSignUpResponse = (obj: unknown): obj is SignUpResponse => (
isObject(obj, {
'Success': isConst(true),
}) || isObject(obj, {
'Success': isConst(false),
'Message': 'string',
})
)
type SignUp = boolean
export type { SignUpBody, SignUpResponse, SignUp }
export { isSignUpResponse }

View File

@ -0,0 +1,23 @@
import { isString } from 'react-bootstrap-typeahead/types/utils'
import { API_URL } from '../../config'
import { fallbackTo } from '../../utils/types'
import { Token, TokenResponse } from './types'
const composeTokenURL = () => (
API_URL + '/token?'
)
const composeSignInBody = (formData: FormData) => {
const resFD = new FormData()
resFD.append('username', fallbackTo(formData.get('email'), isString, ''))
resFD.append('password', fallbackTo(formData.get('password'), isString, ''))
return resFD
}
const processToken = (data: TokenResponse): Token => {
return data.access_token
}
export { composeTokenURL, composeSignInBody, processToken }

View File

@ -0,0 +1,17 @@
import { isObject } from '../../utils/types'
type TokenResponse = {
access_token: string
}
const isTokenResponse = (obj: unknown): obj is TokenResponse => (
isObject(obj, {
'access_token': 'string'
})
)
type Token = string
export type { TokenResponse, Token }
export { isTokenResponse }

View File

@ -1,38 +1,59 @@
import { FormEventHandler } from 'react'
import { FormEventHandler, useCallback } from 'react'
import { Button, Form } from 'react-bootstrap'
import { useSignIn, useSignUp } from '../hooks/api'
import { composeSignUpBody } from '../api/signup'
import { composeSignInBody } from '../api/token'
type AuthFormProps = {
register: boolean
handleAuth: FormEventHandler<HTMLFormElement>,
loading: boolean,
error: string
goBack: () => void,
}
function AuthForm ({ handleAuth, register, loading, error }: AuthFormProps) {
const buttonText = loading ? 'Загрузка...' : (error || (register ? 'Зарегистрироваться' : 'Войти'))
function AuthForm({ goBack, register }: AuthFormProps) {
const { handleSignUp, signUpButton } = useSignUp()
const { handleSignIn, signInButton } = useSignIn()
const handleAuth: FormEventHandler<HTMLFormElement> = useCallback((e) => {
e.preventDefault()
e.stopPropagation()
const formData = new FormData(e.currentTarget)
void (async () => {
const accountCreated = register ? (
await handleSignUp(composeSignUpBody(formData))
) : true
if (accountCreated) {
await handleSignIn(composeSignInBody(formData))
goBack()
}
})()
}, [register, goBack, handleSignUp, handleSignIn])
return (
<Form onSubmit={handleAuth}>
<Form.Group className='mb-3' controlId='email'>
<Form.Label>Почта</Form.Label>
<Form.Control type='email' required />
<Form.Control name='email' type='email' required />
</Form.Group>
{register && <>
<Form.Group className='mb-3' controlId='name'>
<Form.Label>Имя</Form.Label>
<Form.Control type='text' required />
<Form.Control name='name' type='text' required />
</Form.Group>
<Form.Group className='mb-3' controlId='surname'>
<Form.Label>Фамилия</Form.Label>
<Form.Control type='text' required />
<Form.Control name='surname' type='text' required />
</Form.Group>
</>}
<Form.Group className='mb-3' controlId='password'>
<Form.Label>Пароль</Form.Label>
<Form.Control type='password' required />
<Form.Control name='password' type='password' required />
</Form.Group>
{register &&
@ -46,9 +67,9 @@ function AuthForm ({ handleAuth, register, loading, error }: AuthFormProps) {
</Form.Group>
}
<Button variant='success' type='submit'>
{buttonText}
</Button>
<Button variant='success' type='submit' {
...(register ? signUpButton : signInButton)
} />
</Form>
)
}

View File

@ -1,8 +1,9 @@
export { default as useAnnouncements } from './useAnnouncements'
export { default as useBook } from './useBook'
export { default as useAuth } from './useAuth'
export { default as useTrashboxes } from './useTrashboxes'
export { default as useAddAnnouncement } from './useAddAnnouncement'
export { default as useOsmAddresses } from './useOsmAddress'
export { default as useUser } from './useUser'
export { default as useRemoveAnnouncement } from './useRemoveAnnouncement'
export { default as useSignIn } from './useSignIn'
export { default as useSignUp } from './useSignUp'

View File

@ -1,117 +0,0 @@
import { useState } from 'react'
import { API_URL } from '../../config'
import { isConst, isObject } from '../../utils/types'
import { handleHTTPErrors } from '../../utils'
interface AuthData {
email: string,
password: string,
}
// interface LoginData extends AuthData { }
// interface SignUpData extends AuthData {
// name: string,
// surname: string
// }
type SignUpResponse = {
Success: true
} | {
Success: false,
Message: string
}
const isSignUpResponse = (obj: unknown): obj is SignUpResponse => (
isObject(obj, {
'Success': isConst(true)
}) ||
isObject(obj, {
'Success': isConst(false),
'Message': 'string'
})
)
interface LogInResponse {
access_token: string,
token_type: 'bearer'
}
const isLogInResponse = (obj: unknown): obj is LogInResponse => (
isObject(obj, {
'access_token': 'string',
'token_type': isConst('bearer')
})
)
function useAuth() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
async function doAuth(data: AuthData, newAccount: boolean) {
setLoading(true)
if (newAccount) {
try {
const res = await fetch(API_URL + '/signup', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
})
handleHTTPErrors(res)
const signupData: unknown = await res.json()
if (!isSignUpResponse(signupData)) {
throw new Error('Malformed server response')
}
if (signupData.Success === false) {
throw new Error(signupData.Message)
}
} catch (err) {
setError(err instanceof Error ? err.message : err as string)
setLoading(false)
return null
}
}
try {
const res = await fetch(API_URL + '/auth/token' + new URLSearchParams({
username: data.email,
password: data.password
}).toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
const logInData: unknown = await res.json()
if (!isLogInResponse(logInData)) {
throw new Error('Malformed server response')
}
const token = logInData.access_token
setError('')
setLoading(false)
return token
} catch (err) {
setError(err instanceof Error ? err.message : err as string)
setLoading(false)
return null
}
}
return { doAuth, loading, error }
}
export default useAuth

View File

@ -0,0 +1,31 @@
import { useSendWithButton } from '..'
import { composeTokenURL, processToken } from '../../api/token'
import { isTokenResponse } from '../../api/token/types'
import { setToken } from '../../utils/auth'
function useSignIn() {
const { doSend, button } = useSendWithButton(
'Войти',
'Войдено',
false,
composeTokenURL(),
'POST',
false,
isTokenResponse,
processToken,
)
async function handleSignIn(formData: FormData) {
const token = await doSend({}, {
body: formData
})
if (token !== undefined) {
setToken(token)
}
}
return { handleSignIn, signInButton: button }
}
export default useSignIn

View File

@ -0,0 +1,31 @@
import { useSendWithButton } from '..'
import { composeSignUpURL, processSignUp } from '../../api/signup'
import { SignUpBody, isSignUpResponse } from '../../api/signup/types'
function useSignUp() {
const { doSend, button } = useSendWithButton(
'Зарегистрироваться',
'Зарегистрирован',
false,
composeSignUpURL(),
'POST',
false,
isSignUpResponse,
processSignUp,
)
async function handleSignUp(data: SignUpBody) {
const res = await doSend({}, {
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
})
return res ?? false
}
return { handleSignUp, signUpButton: button }
}
export default useSignUp

View File

@ -4,4 +4,5 @@ export { default as useFetch } from './useFetch'
export { default as useStoryIndex } from './useStoryIndex'
export { default as useFilters } from './useFilters'
export { default as useSendWithButton } from './useSendWithButton'
export { default as useSendButtonCaption } from './useSendButtonCaption'
export { default as useId } from './useId'

View File

@ -16,6 +16,8 @@ function useSendWithButton<R, T extends NonNullable<unknown>>(
const data = await doSend(...args)
update(data)
return data
}, [doSend, update])
return { doSend: doSendWithButton, button }

View File

@ -1,50 +1,40 @@
import { FormEventHandler } from 'react'
import { useCallback, useState } from 'react'
import { Card, Tabs, Tab } from 'react-bootstrap'
import { useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../hooks/api';
import { setToken } from '../utils/auth';
import { AuthForm } from '../components';
import { AuthForm } from '../components'
import { isLiteralUnion } from '../utils/types'
const tabKeys = ['register', 'login'] as const
type TabKeys = typeof tabKeys[number]
const isTabKeys = (s: string | null): s is TabKeys => (
isLiteralUnion(s, tabKeys)
)
function LoginPage() {
const navigate = useNavigate()
const { doAuth, loading, error } = useAuth()
const [tab, setTab] = useState<TabKeys>('register')
const handleAuth = (newAccount: boolean): FormEventHandler<HTMLFormElement> => async (event) => {
event.preventDefault();
event.stopPropagation();
const formData = new FormData(event.currentTarget)
const data = {
email: formData.get('email') as string,
name: newAccount ? formData.get('name') as string : undefined,
surname: newAccount ? formData.get('surname') as string : undefined,
password: formData.get('password') as string
}
const token = import.meta.env.PROD ? (
await doAuth(data, newAccount)
) : (
'eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjUifQ.1S46AuB9E4JN9yLkqs30yl3sLlGLbgbrOCNKXiNK8IM'
)
if (token) {
setToken(token)
const goBack = useCallback(() => (
navigate(-1 - Number(import.meta.env.DEV))
}
}
), [navigate])
return (
<Card className='m-4'>
<Card.Body>
<Tabs defaultActiveKey='register' fill justify className='mb-3'>
<Tabs
activeKey={tab}
onSelect={(k) => isTabKeys(k) && setTab(k)}
fill justify
className='mb-3'
>
<Tab eventKey='register' title='Регистрация'>
<AuthForm handleAuth={handleAuth(true)} register={true} loading={loading} error={error} />
<AuthForm goBack={goBack} register={true} />
</Tab>
<Tab eventKey='login' title='Вход'>
<AuthForm handleAuth={handleAuth(false)} register={false} loading={loading} error={error} />
<AuthForm goBack={goBack} register={false} />
</Tab>
</Tabs>
</Card.Body>

View File

@ -33,11 +33,11 @@ function clearToken() {
}
type TokenPayload = {
id: string
user_id: number
}
const isTokenPayload = (data: unknown): data is TokenPayload => isObject(data, {
'id': 'string'
'user_id': 'number'
})
function getId() {
@ -54,7 +54,7 @@ function getId() {
throw new Error('Malformed token payload')
}
const id = Number.parseInt(payload.id)
const id = payload.user_id
if (!isInt(id) || id < 0) {
throw new Error(`Not valid id: ${id}`)

View File

@ -70,8 +70,14 @@ function fallbackToUndefined<T>(obj: unknown, isT: ((obj: unknown) => obj is T))
return obj
}
function fallbackTo<T>(obj: unknown, isT: ((obj: unknown) => obj is T), to: T) {
if (!isT(obj)) return to
return obj
}
type SetState<T> = React.Dispatch<React.SetStateAction<T>>
export type { SetState }
export { isRecord, isObject, isConst, isLiteralUnion, isArrayOf, isString, isInt, fallbackToUndefined }
export { isRecord, isObject, isConst, isLiteralUnion, isArrayOf, isString, isInt, fallbackToUndefined, fallbackTo }