more error handling

This commit is contained in:
Igor Lobanov 2022-11-14 02:17:12 +01:00
parent 11874c6c1d
commit f66015cd77
8 changed files with 111 additions and 17 deletions

View File

@ -15,9 +15,10 @@
.authorDetails { .authorDetails {
display: flex; display: flex;
flex: 1; flex: 1;
// padding-right: 1.2rem;
width: max-content; width: max-content;
// padding-right: 1.2rem;
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
flex-wrap: wrap; flex-wrap: wrap;
} }

View File

@ -1,11 +1,13 @@
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { t } from '../../../utils/intl' import { t } from '../../../utils/intl'
import { hideModal } from '../../../stores/ui' import { hideModal, locale } from '../../../stores/ui'
import { createMemo, onMount, Show } from 'solid-js' import { createMemo, createSignal, onMount, Show } from 'solid-js'
import { useRouter } from '../../../stores/router' import { handleClientRouteLinkClick, useRouter } from '../../../stores/router'
import type { ConfirmEmailSearchParams } from './types' import type { ConfirmEmailSearchParams } from './types'
import { useAuth } from '../../../context/auth' import { signSendLink, useAuth } from '../../../context/auth'
import { ApiError } from '../../../utils/apiClient'
import { email } from './sharedLogic'
export const EmailConfirm = () => { export const EmailConfirm = () => {
const { const {
@ -13,6 +15,9 @@ export const EmailConfirm = () => {
actions: { confirmEmail } actions: { confirmEmail }
} = useAuth() } = useAuth()
const [isTokenExpired, setIsTokenExpired] = createSignal(false)
const [isTokenInvalid, setIsTokenInvalid] = createSignal(false)
const confirmedEmail = createMemo(() => session()?.user?.email || '') const confirmedEmail = createMemo(() => session()?.user?.email || '')
const { searchParams } = useRouter<ConfirmEmailSearchParams>() const { searchParams } = useRouter<ConfirmEmailSearchParams>()
@ -22,23 +27,54 @@ export const EmailConfirm = () => {
try { try {
await confirmEmail(token) await confirmEmail(token)
} catch (error) { } catch (error) {
if (error instanceof ApiError) {
if (error.code === 'token_expired') {
setIsTokenExpired(true)
return
}
if (error.code === 'token_invalid') {
setIsTokenInvalid(true)
return
}
}
console.log(error) console.log(error)
} }
}) })
return ( return (
<div> <div>
<div class={styles.title}>{t('Hooray! Welcome!')}</div> {/* TODO: texts */}
<Show when={isTokenExpired()}>
<div class={styles.title}>Ссылка больше не действительна</div>
<div class={styles.text}>
<a href="/?modal=auth&mode=login" class={styles.sendLink} onClick={handleClientRouteLinkClick}>
{/*TODO: temp solution, should be send link again, but we don't have email here*/}
Вход
</a>
</div>
</Show>
<Show when={isTokenInvalid()}>
<div class={styles.title}>Неправильная ссылка</div>
<div class={styles.text}>
<a href="/?modal=auth&mode=login" class={styles.sendLink} onClick={handleClientRouteLinkClick}>
{/*TODO: temp solution, should be send link again, but we don't have email here*/}
Вход
</a>
</div>
</Show>
<Show when={Boolean(confirmedEmail())}> <Show when={Boolean(confirmedEmail())}>
<div class={styles.title}>{t('Hooray! Welcome!')}</div>
<div class={styles.text}> <div class={styles.text}>
{t("You've confirmed email")} {confirmedEmail()} {t("You've confirmed email")} {confirmedEmail()}
</div> </div>
</Show>
<div> <div>
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}> <button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
{t('Go to main page')} {t('Go to main page')}
</button> </button>
</div> </div>
</Show>
</div> </div>
) )
} }

View File

@ -8,6 +8,7 @@ import type { AuthModalSearchParams } from './types'
import { isValidEmail } from './validators' import { isValidEmail } from './validators'
import { locale } from '../../../stores/ui' import { locale } from '../../../stores/ui'
import { signSendLink } from '../../../context/auth' import { signSendLink } from '../../../context/auth'
import { ApiError } from '../../../utils/apiClient'
type FormFields = { type FormFields = {
email: string email: string
@ -26,11 +27,13 @@ export const ForgotPasswordForm = () => {
const [submitError, setSubmitError] = createSignal('') const [submitError, setSubmitError] = createSignal('')
const [isSubmitting, setIsSubmitting] = createSignal(false) const [isSubmitting, setIsSubmitting] = createSignal(false)
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({}) const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
const [isUserNotFount, setIsUserNotFound] = createSignal(false)
const handleSubmit = async (event: Event) => { const handleSubmit = async (event: Event) => {
event.preventDefault() event.preventDefault()
setSubmitError('') setSubmitError('')
setIsUserNotFound(false)
const newValidationErrors: ValidationErrors = {} const newValidationErrors: ValidationErrors = {}
@ -51,9 +54,12 @@ export const ForgotPasswordForm = () => {
setIsSubmitting(true) setIsSubmitting(true)
try { try {
const result = await signSendLink({ email: email(), lang: locale() }) await signSendLink({ email: email(), lang: locale() })
if (result.error) setSubmitError(result.error)
} catch (error) { } catch (error) {
if (error instanceof ApiError && error.code === 'user_not_found') {
setIsUserNotFound(true)
return
}
setSubmitError(error.message) setSubmitError(error.message)
} finally { } finally {
setIsSubmitting(false) setIsSubmitting(false)
@ -71,6 +77,21 @@ export const ForgotPasswordForm = () => {
</ul> </ul>
</div> </div>
</Show> </Show>
<Show when={isUserNotFount()}>
<div class={styles.authSubtitle}>
{/*TODO: text*/}
{t("We can't find you, check email or")}{' '}
<a
href="#"
onClick={(event) => {
event.preventDefault()
changeSearchParam('mode', 'register')
}}
>
{t('register')}
</a>
</div>
</Show>
<Show when={validationErrors().email}> <Show when={validationErrors().email}>
<div class={styles.validationError}>{validationErrors().email}</div> <div class={styles.validationError}>{validationErrors().email}</div>
</Show> </Show>

View File

@ -57,6 +57,7 @@ export const LoginForm = () => {
event.preventDefault() event.preventDefault()
setIsLinkSent(false) setIsLinkSent(false)
setIsEmailNotConfirmed(false)
setSubmitError('') setSubmitError('')
const newValidationErrors: ValidationErrors = {} const newValidationErrors: ValidationErrors = {}

View File

@ -20,6 +20,9 @@ const AuthContext = createContext<AuthContextType>()
const refreshSession = async (): Promise<AuthResult> => { const refreshSession = async (): Promise<AuthResult> => {
try { try {
const authResult = await apiClient.getSession() const authResult = await apiClient.getSession()
if (!authResult) {
return null
}
setToken(authResult.token) setToken(authResult.token)
return authResult return authResult
} catch (error) { } catch (error) {

View File

@ -10,6 +10,10 @@ if (isDev) {
exchanges.unshift(devtoolsExchange) exchanges.unshift(devtoolsExchange)
} }
export const getToken = (): string => {
return localStorage.getItem(TOKEN_LOCAL_STORAGE_KEY)
}
export const setToken = (token: string) => { export const setToken = (token: string) => {
localStorage.setItem(TOKEN_LOCAL_STORAGE_KEY, token) localStorage.setItem(TOKEN_LOCAL_STORAGE_KEY, token)
} }

View File

@ -169,5 +169,7 @@
"Send link again": "Прислать ссылку ещё раз", "Send link again": "Прислать ссылку ещё раз",
"Link sent, check your email": "Ссылка отправлена, проверьте почту", "Link sent, check your email": "Ссылка отправлена, проверьте почту",
"Create post": "Создать публикацию", "Create post": "Создать публикацию",
"Just start typing...": "Просто начните печатать..." "Just start typing...": "Просто начните печатать...",
"We can't find you, check email or": "Не можем вас найти, проверьте адрес электронной почты или",
"register": "зарегистрируйтесь"
} }

View File

@ -8,7 +8,7 @@ import type {
Author Author
} from '../graphql/types.gen' } from '../graphql/types.gen'
import { publicGraphQLClient } from '../graphql/publicGraphQLClient' import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
import { privateGraphQLClient } from '../graphql/privateGraphQLClient' import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient'
import articleBySlug from '../graphql/query/article-by-slug' import articleBySlug from '../graphql/query/article-by-slug'
import articlesRecentAll from '../graphql/query/articles-recent-all' import articlesRecentAll from '../graphql/query/articles-recent-all'
import articlesRecentPublished from '../graphql/query/articles-recent-published' import articlesRecentPublished from '../graphql/query/articles-recent-published'
@ -41,7 +41,13 @@ import topicBySlug from '../graphql/query/topic-by-slug'
const FEED_SIZE = 50 const FEED_SIZE = 50
type ApiErrorCode = 'unknown' | 'email_not_confirmed' | 'user_not_found' | 'user_already_exists' type ApiErrorCode =
| 'unknown'
| 'email_not_confirmed'
| 'user_not_found'
| 'user_already_exists'
| 'token_expired'
| 'token_invalid'
export class ApiError extends Error { export class ApiError extends Error {
code: ApiErrorCode code: ApiErrorCode
@ -109,16 +115,32 @@ export const apiClient = {
const response = await publicGraphQLClient.mutation(authSendLinkMutation, { email, lang }).toPromise() const response = await publicGraphQLClient.mutation(authSendLinkMutation, { email, lang }).toPromise()
if (response.error) { if (response.error) {
if (response.error.message === '[GraphQL] User not found') {
throw new ApiError('user_not_found', response.error.message)
}
throw new ApiError('unknown', response.error.message) throw new ApiError('unknown', response.error.message)
} }
if (response.data.sendLink.error) {
throw new ApiError('unknown', response.data.sendLink.message)
}
return response.data.sendLink return response.data.sendLink
}, },
confirmEmail: async ({ token }: { token: string }) => { confirmEmail: async ({ token }: { token: string }) => {
// confirm email with code from link // confirm email with code from link
const response = await publicGraphQLClient.mutation(authConfirmEmailMutation, { token }).toPromise() const response = await publicGraphQLClient.mutation(authConfirmEmailMutation, { token }).toPromise()
if (response.error) { if (response.error) {
// TODO: better error communication
if (response.error.message === '[GraphQL] check token lifetime') {
throw new ApiError('token_expired', response.error.message)
}
if (response.error.message === '[GraphQL] token is not valid') {
throw new ApiError('token_invalid', response.error.message)
}
throw new ApiError('unknown', response.error.message) throw new ApiError('unknown', response.error.message)
} }
@ -251,6 +273,10 @@ export const apiClient = {
}, },
getSession: async (): Promise<AuthResult> => { getSession: async (): Promise<AuthResult> => {
if (!getToken()) {
return null
}
// renew session with auth token in header (!) // renew session with auth token in header (!)
const response = await privateGraphQLClient.mutation(mySession, {}).toPromise() const response = await privateGraphQLClient.mutation(mySession, {}).toPromise()