@@ -3,6 +3,9 @@ const observedRequests = [ '/api/auth/csrf', '/api/auth/callback/credentials', '/api/auth/session', + '/user-balance', + 'media/image', + 'messagesdsd', ] const failedRequests = [] @@ -33,9 +36,10 @@ self.addEventListener('fetch', (event) => { response.status >= 400 && observedRequests.some((url) => event.request.url.includes(url)) ) { + console.log(await event.request.clone().text()) sendMessageToClients({ url: event.request.url, - reason: await response.text(), + reason: await event.request.clone().text(), status: response.status, }) } @@ -44,4 +48,4 @@ self.addEventListener('fetch', (event) => { } catch (error) {} })() ) -}) \ No newline at end of file +}) @@ -124,35 +124,57 @@ p { } .MuiPaper-root.MuiAutocomplete-paper { - background-color: var(--new-ui-main-color); /* Замените #your-color на ваш цвет */ - border: 1px solid var(--new-ui-main-color); /* Замените #your-border-color на цвет бордюра */ - box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.15); /* Измените тень, если необходимо */ + background-color: var(--new-ui-main-color); + /* Замените #your-color на ваш цвет */ + border: 1px solid var(--new-ui-main-color); + /* Замените #your-border-color на цвет бордюра */ + box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.15); + /* Измените тень, если необходимо */ +} + +.MuiInputBase-input:-webkit-autofill, +.MuiInputBase-input:-webkit-autofill:hover, +.MuiInputBase-input:-webkit-autofill:focus, +.MuiInputBase-input:-webkit-autofill:active { + /* box-shadow: 0 0 0 100px var(--new-ui-main-color) inset !important; */ + -webkit-background-clip: text; + -webkit-text-fill-color: var(--text-color-main); + transition: background-color 5000s ease-in-out 0s; + box-shadow: inset 0 0 0 100px var(--new-ui-main-color); + border-radius: 15px; } /* Изменение цвета полоски сверху выпадающего списка */ .MuiAutocomplete-listbox:before { - border-top: 1px solid var(--new-ui-main-color); /* Замените #your-border-color на цвет бордюра */ + border-top: 1px solid var(--new-ui-main-color); + /* Замените #your-border-color на цвет бордюра */ } /* Изменение цвета полоски снизу выпадающего списка */ .MuiAutocomplete-listbox:after { - border-bottom: 1px solid var(--new-ui-main-color); /* Замените #your-border-color на цвет бордюра */ + border-bottom: 1px solid var(--new-ui-main-color); + /* Замените #your-border-color на цвет бордюра */ } .MuiAutocomplete-popup { - background-color: var(--new-ui-main-color); /* Замените #your-color на ваш цвет */ + background-color: var(--new-ui-main-color); + /* Замените #your-color на ваш цвет */ } /* Изменение цвета элементов в выпадающем списке */ .MuiAutocomplete-option { - background-color: var(--new-ui-main-color); /* Замените #your-color на ваш цвет */ - color: var(--new-ui-text-color); /* Замените #your-text-color на цвет текста элементов */ + background-color: var(--new-ui-main-color); + /* Замените #your-color на ваш цвет */ + color: var(--new-ui-text-color); + /* Замените #your-text-color на цвет текста элементов */ } /* Изменение цвета активного элемента в выпадающем списке */ .MuiAutocomplete-option.Mui-selected { - background-color: var(--new-ui-main-color); /* Замените #your-selected-color на цвет активного элемента */ - color: var(--new-ui-main-color); /* Замените #your-selected-text-color на цвет текста активного элемента */ + background-color: var(--new-ui-main-color); + /* Замените #your-selected-color на цвет активного элемента */ + color: var(--new-ui-main-color); + /* Замените #your-selected-text-color на цвет текста активного элемента */ } .introjs-tooltiptext { @@ -309,6 +331,7 @@ textarea { .rdw-option-wrapper { border: none !important; } + .rdw-option-wrapper:hover { box-shadow: none !important; } @@ -354,9 +377,11 @@ textarea { color: inherit !important; overflow-y: hidden !important; } + .rdw-block-dropdown { width: 150px !important; } + .rdw-dropdown-optionwrapper > li { color: inherit !important; } @@ -414,3 +439,21 @@ textarea { transition: all; transition-duration: 250ms; } + +.my-node-enter { + opacity: 0; +} + +.my-node-enter-active { + opacity: 1; + transition: opacity 400ms; +} + +.my-node-exit { + opacity: 1; +} + +.my-node-exit-active { + opacity: 0; + transition: opacity 400ms; +} @@ -0,0 +1,7 @@ + + + + + + + @@ -0,0 +1,4 @@ + + + + @@ -0,0 +1,7 @@ +.modalContainer { + min-width: 300px; + min-height: 200px; + background: var(--background-color-additional); + border-radius: 15px; + padding: 15px; +} \ No newline at end of file @@ -9,11 +9,11 @@ export const useLibrarySwiper = (onSlideFalse: ((...args: any) => any) | undefin const keydown = (e: KeyboardEvent) => { if (e.key === 'ArrowRight') { - e.preventDefault() + // e.preventDefault() slideNext() } if (e.key === 'ArrowLeft') { - e.preventDefault() + // e.preventDefault() slidePrev() } } @@ -0,0 +1 @@ +export * from './model-input' \ No newline at end of file @@ -0,0 +1,62 @@ +.wrapper { + display: flex; + align-items: center; + padding: 14px 14px; + width: 100%; + border-radius: 15px; + gap: 10px; +} + +:root[data-theme='light'] { + .wrapper { + border: 2px solid #e1e1e1; + } + .divider { + background-color: #EBEFF5; + } +} + +:root[data-theme='dark'] { + .wrapper { + border: 2px solid rgb(66, 66, 72) !important; + } + .divider { + background-color: #40404E; + } +} + +.area { + width: 100%; + resize: none; + // height: 28px; + border-radius: 0px; + overflow-y: hidden; + padding: 0px; + font-size: 16px; + max-height: 82px; + + &::placeholder{ + font-size: 16px; + } + + border: none; + outline: none; + &:focus { + outline: none; + } +} + +.endAdornment { + display: flex; + align-items: center; +} + +.divider { + height: 20px; + width: 1px; + margin: 0px 8px; +} + +// theme === 'dark' + // ? '#40404E' + // : '#EBEFF5', \ No newline at end of file @@ -11,6 +11,8 @@ import { IModelInputs } from '#/shared/api/models/models' import { styleInputWithoutBorderFocus } from '#/shared/ui/input' import { useThemeAndDevice } from '#/shared/lib/hooks' +import classes from './model-input.module.scss' + interface Input { loading: boolean // requestImage: () => void @@ -33,7 +35,7 @@ interface Input { resendValue?: string } -export const UniqInput: FC = ({ +export const ModelInput: FC = ({ image, loading, unpinImage, @@ -55,11 +57,6 @@ export const UniqInput: FC = ({ const [value, setValue] = React.useState('') useEffect(() => { - console.log(loading) - }, [loading]) - // const [disableOnChange, setDisableOnChange] = React.useState(false) - - React.useEffect(() => { if (input_types) { setRequired(input_types.map((el) => (el.required ? el.type : null))) setTypes(input_types.map((el) => el.type)) @@ -67,7 +64,7 @@ export const UniqInput: FC = ({ } }, [input_types]) - React.useEffect(() => { + useEffect(() => { if (input_types && typeVersions && !typeVersions['text']) { setDisabled(true) } else if ( @@ -78,167 +75,94 @@ export const UniqInput: FC = ({ } }, [typeVersions]) - React.useEffect(() => { + useEffect(() => { if (resendValue) { setValue(resendValue) } }, [resendValue]) - // const handlePaste = (event:ClipboardEvent) => { - // setDisableOnChange(true); - // // const pastedData = ` \n \`\`\`text \n ${event.clipboardData.getData('Text')} \n \`\`\``; - // const pastedData = event.clipboardData.getData('Text') - // setValue(value + pastedData) - // // Включаем onChange обратно после обработки вставки - // setTimeout(() => setDisableOnChange(false), 0); - // }; - return ( - { - if (e.shiftKey && e.key === 'Enter') { - } else if (e.key === 'Enter') { - e.preventDefault() - const isSend = sendMessage(value, required) - if (isSend) { - setValue('') - if (unpinImage) unpinImage() + > + {!desktop && ( + + + + )} + + +
+ {typeVersions['image'] && + (typeVersions['image'].length === 0 || + typeVersions['image'].includes(currentVersion)) && ( + <> + {!blocked && ( + )} - - - ), - startAdornment: !desktop && ( - - - - - - ), - }} - /> +
+ + )} + {!disabled && !blocked && ( + + )} +
+ ) } @@ -0,0 +1 @@ +export * from './ui' \ No newline at end of file @@ -2,7 +2,7 @@ import React, { useState } from 'react' import { useCookies } from 'react-cookie' import { useForm } from 'react-hook-form' import { SubmitErrorHandler, SubmitHandler } from 'react-hook-form/dist/types/form' -import { Button, Checkbox, TextField } from '@mui/material' +import { Button, Checkbox, InputAdornment, TextField } from '@mui/material' import Box from '@mui/material/Box' import CircularProgress from '@mui/material/CircularProgress' import Typography from '@mui/material/Typography' @@ -20,6 +20,8 @@ import { InputStyleDark, InputStyleLight } from '#/shared' import { API_URL } from '#/shared/lib/constants/constants' import { useShowData } from '#/shared/lib/hooks' import { useThemeAndDevice } from '#/shared/lib/hooks' +import OpenedEyeSvg from '#/assets/svg/opened-eye.svg?react' +import ClosedEyeSvg from '#/assets/svg/closed-eye.svg?react' interface IRegisterEmailFormProps { successLogin: () => void @@ -41,14 +43,16 @@ export const RegisterEmailForm: React.FC = ({ successLo const [isEmailWhite, setIsEmailWhite] = React.useState(true) const [cookie] = useCookies() + const [passwordShowed, showPassword] = React.useState(false) const makeNewAccount: SubmitHandler = async (data) => { setLoading(true) if (!data.rules) { - showError('Запрос отправлен', true) try { - const { status } = await axios.get(API_URL + `/auth/mail/whitelist?email=${data.email}`) + const { status } = await axios.get( + API_URL + `/auth/mail/whitelist?email=${data.email}`, { validateStatus: (status) => status < 400 } + ) if (status === 200) { setIsEmailWhite(true) } @@ -83,7 +87,7 @@ export const RegisterEmailForm: React.FC = ({ successLo } try { - const { status } = await axios.post(API_URL + '/auth/register', req_data) + const { status } = await axios.post(API_URL + '/auth/register', req_data, { validateStatus: (status) => status < 400 }) if (status === 201) { setLoading(false) reset() @@ -149,8 +153,28 @@ export const RegisterEmailForm: React.FC = ({ successLo +
showPassword((x) => !x)} + style={{ + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + }} + > + {passwordShowed ? ( + + ) : ( + + )} +
+ + ), + }} sx={theme === 'light' ? { ...InputStyleLight } : { ...InputStyleDark }} {...register('password1', { ...PasswordOptions, @@ -173,7 +197,7 @@ export const RegisterEmailForm: React.FC = ({ successLo = ({ successLo })} /> - + => { - try { - const { data } = await axios.get( - API_URL + `/payments/expenses?source_strategy=${type}&interval_strategy=${interval}`, - { - headers: { Authorization: `Bearer ${token}` }, - } - ) - return data - } catch (e) { - return { - details: '', - error: true, - message: 'Ошибка загрузки статистики', +const statsColors = [ + '#8280FF', + '#67D9A2', + '#FF867E', + '#7ED1FF', + '#FFD700', + '#FFA07A', + '#87CEEB', + '#98FB98', + '#FFB6C1', +] + +const getStatistic = async (type: string, interval: string, token?: string) => { + return await axios.get( + API_URL + `/payments/expenses?source_strategy=${type}&interval_strategy=${interval}`, + { + headers: { Authorization: `Bearer ${token}` }, + validateStatus: () => true, } - } + ) } export const useStats = () => { @@ -88,48 +90,47 @@ export const useStats = () => { const [statsByDays, setStatsByDays] = useState(dataLine) - function setStats(type: string) { - getStatistic(type, timeForStats, session?.access).then((res) => { - if ('error' in res) return - - const spends = res.map((el) => el.amount).filter((el) => +el) - - if (type === 'days') { - const sortedData = formatAndSortDates(res) - - dataLine = { - labels: sortedData.map((el: any) => el.formattedDate), - datasets: [ - { - ...dataLine.datasets[0], - fill: true, - //@ts-ignore - data: sortedData.map((el: any) => el.amount), - }, - ], - } - - setStatsByDays(dataLine) - return - } + async function setStats(type: string) { + const { data: res, status } = await getStatistic(type, timeForStats, session?.access) + if (status != 200) return + + const spends = res.map((el) => el.amount).filter((el) => +el) + + if (type === 'days') { + const sortedData = formatAndSortDates(res) - dataForAround = { - ...dataForAround, - labels: (res as never[]).filter((el: any) => +el.amount), + dataLine = { + labels: sortedData.map((el: any) => el.formattedDate), datasets: [ { - label: '', - data: spends, - backgroundColor: statsColors.slice(0, spends.length), - borderColor: 'transparent', - borderRadius: 8, - } as never, + ...dataLine.datasets[0], + fill: true, + //@ts-ignore + data: sortedData.map((el: any) => el.amount), + }, ], } - if (type === 'models') setStatsByModels(dataForAround) - if (type === 'categories') setStatsByType(dataForAround) - }) + setStatsByDays(dataLine) + return + } + + dataForAround = { + ...dataForAround, + labels: (res as never[]).filter((el: any) => +el.amount), + datasets: [ + { + label: '', + data: spends, + backgroundColor: statsColors.slice(0, spends.length), + borderColor: 'transparent', + borderRadius: 8, + } as never, + ], + } + + if (type === 'models') setStatsByModels(dataForAround) + if (type === 'categories') setStatsByType(dataForAround) } useEffect(() => { @@ -142,7 +143,20 @@ export const useStats = () => { } function formatAndSortDates(inputArray: any) { - const monthNames = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'] + const monthNames = [ + 'янв', + 'фев', + 'мар', + 'апр', + 'май', + 'июн', + 'июл', + 'авг', + 'сен', + 'окт', + 'ноя', + 'дек', + ] const formattedData = inputArray.map((item: any) => { const date = new Date(item.source) @@ -2,13 +2,17 @@ import NextAuth, { NextAuthOptions, User } from 'next-auth' import CredentialsProvider from 'next-auth/providers/credentials' import YandexProvider from 'next-auth/providers/yandex' -import { AuthConstants } from './constants' +import { AuthConstants, ERROR_MAPPING, ERROR_YANDEX_MAPPING } from './constants' import { AuthorizationProxy } from './proxy' +import * as Sentry from '@sentry/nextjs' +import { getAll } from '#/entities/user-account/model/user-type-slice' + const AuthProxy = new AuthorizationProxy() export const authOptions: NextAuthOptions = { session: { strategy: 'jwt', maxAge: AuthConstants.sessionTime }, + secret: process.env.NEXTAUTH_SECRET, providers: [ YandexProvider({ clientId: AuthConstants.client_id_yandex || '', @@ -20,32 +24,54 @@ export const authOptions: NextAuthOptions = { id: 'credentials', type: 'credentials', credentials: {}, - async authorize(credentials, req): Promise { + async authorize(credentials, req) { const { username, password } = credentials as any - const user = await AuthProxy.loginByEmail(username, password) - return user ?? null + const resp = await AuthProxy.loginByEmail(username, password) + const data = await resp.json() + + if (resp.status >= 400 && resp.status < 500) { + throw new Error( + ERROR_MAPPING[(data as { detail: string }).detail] ?? + JSON.stringify(data as Object) + .replace(/\s+/, '_') + .replace(/[А-Яа-я]/, '') + .toLowerCase() + ) + } + return (await data) as User }, }), ], callbacks: { - async jwt({ token, user, account }) { + async jwt({ token, user, account, trigger }) { if (account?.provider === 'yandex') { const result = await AuthProxy.exchangeTokenYandex(account.access_token || '') - const { access, refresh } = result + if (result.status >= 400 && result.status < 500) { + throw new Error('Непредвиденная ошибка') + } + + const info = await getAll( + (result.data as { access_token: string }).access_token + ) - token.access = access - token.refresh = refresh - token.tokenExpiry = AuthConstants.getExpiresDate() - return token + console.log(info) + + console.log(trigger) + + return { ...info.token, tokenExpiry: AuthConstants.getExpiresDate() } } if (user) { - token.access = user.token.access - token.refresh = user.token.refresh - token.tokenExpiry = AuthConstants.getExpiresDate() - return token + try { + token.access = user.token.access + token.refresh = user.token.refresh + token.tokenExpiry = AuthConstants.getExpiresDate() + return token + } catch (error) { + return { ...token, ...user } + } } const shouldRefreshTime = Math.round(Date.now() - token.tokenExpiry) >= 0 @@ -56,17 +82,36 @@ export const authOptions: NextAuthOptions = { return token }, + signIn: async ({ user, account, ...props }) => { + if (account?.provider === 'yandex') { + const { data: result, status } = await AuthProxy.exchangeTokenYandex( + account.access_token! + ) + + if (status >= 400 && status < 500) { + return Promise.reject( + new Error( + ERROR_YANDEX_MAPPING[ + (result as { error_description: string }).error_description + ] + ) + ) + } + + return Promise.resolve(true) + } + + return Promise.resolve(true) + }, async session({ session, token }) { session.access = token.access session.refresh = token.refresh return session }, - async redirect({ url, baseUrl }) { - return baseUrl - }, }, pages: { signIn: '/login', + error: '/login', }, } @@ -3,9 +3,11 @@ import process from 'process' export abstract class AuthConstants { public static readonly redirect_uri_yandex = `https://oauth.yandex.ru/authorize?&redirect_uri=${process.env.NEXT_PUBLIC_REDIRECT_URI_YANDEX}` - public static readonly django_app_client_id_yandex = process.env.NEXT_PUBLIC_DJANGO_YANDEX_APP_CLIENT_ID + public static readonly django_app_client_id_yandex = + process.env.NEXT_PUBLIC_DJANGO_YANDEX_APP_CLIENT_ID - public static readonly django_app_client_secret_yandex = process.env.NEXT_PUBLIC_DJANGO_YANDEX_APP_CLIENT_SECRET + public static readonly django_app_client_secret_yandex = + process.env.NEXT_PUBLIC_DJANGO_YANDEX_APP_CLIENT_SECRET public static readonly client_id_yandex = process.env.NEXT_PUBLIC_CLIENT_ID_YANDEX @@ -20,3 +22,20 @@ export abstract class AuthConstants { public static readonly getExpiresDate = () => Date.now() + this.expiresTime } + +// TODO: перекинуть slug-коды на бэк +export const ERROR_MAPPING: Record = { + 'Неверный email': 'email_not_correct', + 'Неверный пароль': 'password_not_correct', + 'Пользователь пока не подтвердил свой email': 'email_not_approved', +} + +export const ERROR_YANDEX_MAPPING: Record = { + "Your credentials aren't allowed": 'yandex_credentials_not_allowed', + 'Invalid client_id parameter value.': 'yandex_client_id_not_correct', +} + +export const ERRROR_YANDEX_TRANSLATE_MAPPING: Record = { + 'yandex_credentials_not_allowed': 'Неверные учетные данные', + 'yandex_client_id_not_correct': 'Ошибка сервера (скоро поправим)', +} @@ -1,7 +1,7 @@ import axios, { AxiosResponse } from 'axios' import { User } from 'next-auth' import { JWT } from 'next-auth/jwt' -import * as Sentry from "@sentry/browser"; +import * as Sentry from '@sentry/browser' import { getAll } from '#/entities/user-account/model/user-type-slice' import { API_URL } from '#/shared/lib/constants' @@ -13,34 +13,47 @@ type ExchangeReturn = { access: string; refresh: string } type DataForLogin = { username: string; password: string } export class AuthorizationProxy { - public readonly loginByEmail = async (email: string, password: string): Promise => { - try { - const { data } = await axios.post>(API_URL + '/auth/login', { + public readonly loginByEmail = async (email: string, password: string) => { + return await fetch(API_URL + '/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, password, - }, {timeout: 25000}) - - return data - } catch (err:any) { - if (err.code === 'ECONNABORTED'){ - Sentry.captureMessage("ServerError: 25 sec timeout error in AuthorizationProxy.loginbyEmail"); - } - return null - } + }), + }) } - public readonly exchangeTokenYandex = async (yandex_token: string): Promise => { - const { data } = await axios.post(API_URL + '/auth/login-social/convert-token', { - grant_type: 'convert_token', - client_id: AuthConstants.django_app_client_id_yandex, - client_secret: AuthConstants.django_app_client_secret_yandex, - backend: 'yandex-oauth2', - token: yandex_token, - }) + public readonly exchangeTokenYandex = async (yandex_token: string) => { + return await axios.post<{ access_token: string } | { error_description: string }>( + API_URL + '/auth/login-social/convert-token', + { + grant_type: 'convert_token', + client_id: AuthConstants.django_app_client_id_yandex, + client_secret: AuthConstants.django_app_client_secret_yandex, + backend: 'yandex-oauth2', + token: yandex_token, + }, + { validateStatus: () => true } + ) + + // const finalData = await getAll(data.access_token) - const finalData = await getAll(data.access_token) + // return finalData.token + } - return finalData.token + public readonly exchangeTokenYandexTest = async (yandex_token: string) => { + return await axios.get('https://login.yandex.ru/info', { + params: { + format: 'json', + jwt_secret: AuthConstants.django_app_client_secret_yandex, + }, + headers: { + Authorization: `OAuth ${yandex_token}`, + }, + }) } public readonly refreshToken = async (tokenObject: JWT): Promise => { @@ -38,35 +38,16 @@ export interface AppPropsWithLayout extends AppProps { function App({ Component, pageProps: { session, ...pageProps } }: AppPropsWithLayout) { const getLayout = Component.getLayout ?? ((page) => page) - const callback = useCallback((event: any) => { - Sentry.captureMessage(`error in response - - ${event.data.url} - - ${event.data.status} - ${event.data.reason}`) - - console.log('Ответ от Service Worker:', event.data) - }, []) - - function registerSw() { - navigator.serviceWorker.register('sw.js').then((registration) => { - registration.onupdatefound = () => { - const newWorker = registration.installing! - - newWorker.onstatechange = () => { - if (newWorker.state === 'installed') { - if (navigator.serviceWorker.controller) { - registration.unregister().then(() => { - window.location.reload() - }) - } - } - } + if ('serviceWorker' in navigator) { + navigator.serviceWorker.getRegistrations().then(function (registrations) { + for (let registration of registrations) { + registration.unregister(); } - }) - navigator.serviceWorker.addEventListener('message', callback) + }).catch(function (error) { + console.error('Ошибка при размонтировании service worker:', error); + }); } - useEffect(registerSw, []) - return ( <>