diff --git a/src/components/Author/Card.module.scss b/src/components/Author/Card.module.scss index bb45951c..b51d4c13 100644 --- a/src/components/Author/Card.module.scss +++ b/src/components/Author/Card.module.scss @@ -15,9 +15,10 @@ .authorDetails { display: flex; flex: 1; - // padding-right: 1.2rem; width: max-content; + // padding-right: 1.2rem; + @include media-breakpoint-down(sm) { flex-wrap: wrap; } diff --git a/src/components/Nav/AuthModal/EmailConfirm.tsx b/src/components/Nav/AuthModal/EmailConfirm.tsx index d458ca3b..ac93ae77 100644 --- a/src/components/Nav/AuthModal/EmailConfirm.tsx +++ b/src/components/Nav/AuthModal/EmailConfirm.tsx @@ -1,11 +1,13 @@ import styles from './AuthModal.module.scss' import { clsx } from 'clsx' import { t } from '../../../utils/intl' -import { hideModal } from '../../../stores/ui' -import { createMemo, onMount, Show } from 'solid-js' -import { useRouter } from '../../../stores/router' +import { hideModal, locale } from '../../../stores/ui' +import { createMemo, createSignal, onMount, Show } from 'solid-js' +import { handleClientRouteLinkClick, useRouter } from '../../../stores/router' 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 = () => { const { @@ -13,6 +15,9 @@ export const EmailConfirm = () => { actions: { confirmEmail } } = useAuth() + const [isTokenExpired, setIsTokenExpired] = createSignal(false) + const [isTokenInvalid, setIsTokenInvalid] = createSignal(false) + const confirmedEmail = createMemo(() => session()?.user?.email || '') const { searchParams } = useRouter() @@ -22,23 +27,54 @@ export const EmailConfirm = () => { try { await confirmEmail(token) } 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) } }) return (
-
{t('Hooray! Welcome!')}
+ {/* TODO: texts */} + +
Ссылка больше не действительна
+ +
+ +
Неправильная ссылка
+ +
+
{t('Hooray! Welcome!')}
{t("You've confirmed email")} {confirmedEmail()}
+
+ +
-
- -
) } diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx index d6b3621e..81116779 100644 --- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -8,6 +8,7 @@ import type { AuthModalSearchParams } from './types' import { isValidEmail } from './validators' import { locale } from '../../../stores/ui' import { signSendLink } from '../../../context/auth' +import { ApiError } from '../../../utils/apiClient' type FormFields = { email: string @@ -26,11 +27,13 @@ export const ForgotPasswordForm = () => { const [submitError, setSubmitError] = createSignal('') const [isSubmitting, setIsSubmitting] = createSignal(false) const [validationErrors, setValidationErrors] = createSignal({}) + const [isUserNotFount, setIsUserNotFound] = createSignal(false) const handleSubmit = async (event: Event) => { event.preventDefault() setSubmitError('') + setIsUserNotFound(false) const newValidationErrors: ValidationErrors = {} @@ -51,9 +54,12 @@ export const ForgotPasswordForm = () => { setIsSubmitting(true) try { - const result = await signSendLink({ email: email(), lang: locale() }) - if (result.error) setSubmitError(result.error) + await signSendLink({ email: email(), lang: locale() }) } catch (error) { + if (error instanceof ApiError && error.code === 'user_not_found') { + setIsUserNotFound(true) + return + } setSubmitError(error.message) } finally { setIsSubmitting(false) @@ -71,6 +77,21 @@ export const ForgotPasswordForm = () => { + +
+ {/*TODO: text*/} + {t("We can't find you, check email or")}{' '} + { + event.preventDefault() + changeSearchParam('mode', 'register') + }} + > + {t('register')} + +
+
{validationErrors().email}
diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index 92d5cfd0..50669263 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -57,6 +57,7 @@ export const LoginForm = () => { event.preventDefault() setIsLinkSent(false) + setIsEmailNotConfirmed(false) setSubmitError('') const newValidationErrors: ValidationErrors = {} diff --git a/src/context/auth.tsx b/src/context/auth.tsx index 7fe11945..71792145 100644 --- a/src/context/auth.tsx +++ b/src/context/auth.tsx @@ -20,6 +20,9 @@ const AuthContext = createContext() const refreshSession = async (): Promise => { try { const authResult = await apiClient.getSession() + if (!authResult) { + return null + } setToken(authResult.token) return authResult } catch (error) { diff --git a/src/graphql/privateGraphQLClient.ts b/src/graphql/privateGraphQLClient.ts index 0de1202a..442343cf 100644 --- a/src/graphql/privateGraphQLClient.ts +++ b/src/graphql/privateGraphQLClient.ts @@ -10,6 +10,10 @@ if (isDev) { exchanges.unshift(devtoolsExchange) } +export const getToken = (): string => { + return localStorage.getItem(TOKEN_LOCAL_STORAGE_KEY) +} + export const setToken = (token: string) => { localStorage.setItem(TOKEN_LOCAL_STORAGE_KEY, token) } diff --git a/src/locales/ru.json b/src/locales/ru.json index e8b1a94d..a7644c70 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -169,5 +169,7 @@ "Send link again": "Прислать ссылку ещё раз", "Link sent, check your email": "Ссылка отправлена, проверьте почту", "Create post": "Создать публикацию", - "Just start typing...": "Просто начните печатать..." + "Just start typing...": "Просто начните печатать...", + "We can't find you, check email or": "Не можем вас найти, проверьте адрес электронной почты или", + "register": "зарегистрируйтесь" } diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index 8f8e65eb..61769847 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -8,7 +8,7 @@ import type { Author } from '../graphql/types.gen' 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 articlesRecentAll from '../graphql/query/articles-recent-all' import articlesRecentPublished from '../graphql/query/articles-recent-published' @@ -41,7 +41,13 @@ import topicBySlug from '../graphql/query/topic-by-slug' 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 { code: ApiErrorCode @@ -109,16 +115,32 @@ export const apiClient = { const response = await publicGraphQLClient.mutation(authSendLinkMutation, { email, lang }).toPromise() 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) } + if (response.data.sendLink.error) { + throw new ApiError('unknown', response.data.sendLink.message) + } + return response.data.sendLink }, confirmEmail: async ({ token }: { token: string }) => { // confirm email with code from link const response = await publicGraphQLClient.mutation(authConfirmEmailMutation, { token }).toPromise() - 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) } @@ -251,6 +273,10 @@ export const apiClient = { }, getSession: async (): Promise => { + if (!getToken()) { + return null + } + // renew session with auth token in header (!) const response = await privateGraphQLClient.mutation(mySession, {}).toPromise()