@@ -1,32 +1,14 @@ -import axios, { AxiosResponse } from 'axios' - -import { getEnv } from '#/shared/lib/env-store' import { DaDataResponse } from './types' -export const getCompanyData = async (prompt: any) => { - const env = getEnv() - const url = env?.NEXT_PUBLIC_URL_DADATA - const token = env?.NEXT_PUBLIC_TOKEN_DADATA - - if (!token || !url) { - return null - } - +export const getCompanyData = async (prompt: string): Promise => { try { - const { data } = await axios.post>( - url, - JSON.stringify({ query: prompt }), - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - Authorization: 'Token ' + token, - }, - } - ) - return data - } catch (err) { + const res = await fetch('/api/dadata/company', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: prompt }), + }) + return (await res.json()) as DaDataResponse + } catch { return null } } @@ -11,7 +11,7 @@ const AuthProxy = new AuthorizationProxy() export const authOptions: NextAuthOptions = { session: { strategy: 'jwt', maxAge: AuthConstants.sessionTime }, - secret: process.env.NEXTAUTH_SECRET, + secret: AuthConstants.secret, providers: [ YandexProvider({ clientId: AuthConstants.client_id_yandex || '', @@ -1,20 +1,23 @@ -import process from 'process' +import type { Env } from '#/shared/lib/env-types' +import { getServerEnv } from '#/shared/lib/server-env' + +const env = getServerEnv() 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 secret = env.NEXTAUTH_SECRET - public static readonly django_app_client_id_yandex = - process.env.NEXT_PUBLIC_DJANGO_YANDEX_APP_CLIENT_ID + public static readonly redirect_uri_yandex = `https://oauth.yandex.ru/authorize?&redirect_uri=${env.NEXT_REDIRECT_URI_YANDEX}` - public static readonly django_app_client_secret_yandex = - process.env.NEXT_PUBLIC_DJANGO_YANDEX_APP_CLIENT_SECRET + public static readonly django_app_client_id_yandex = env.NEXT_DJANGO_YANDEX_APP_CLIENT_ID - public static readonly client_id_yandex = process.env.NEXT_PUBLIC_CLIENT_ID_YANDEX + public static readonly django_app_client_secret_yandex = env.NEXT_DJANGO_YANDEX_APP_CLIENT_SECRET - public static readonly client_secret_yandex = process.env.NEXT_PUBLIC_CLIENT_SECRET_YANDEX + public static readonly client_id_yandex = env.NEXT_CLIENT_ID_YANDEX - public static readonly sessionTime = Number(process.env.NEXT_PUBLIC_SESSION_TIME) * 1000 + public static readonly client_secret_yandex = env.NEXT_CLIENT_SECRET_YANDEX + public static readonly sessionTime = + Number(env.NEXT_SESSION_TIME ?? env.NEXT_PUBLIC_SESSION_TIME) * 1000 private static readonly expiresTime = this.sessionTime @@ -0,0 +1,38 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import axios from 'axios' + +import { getApiUrl } from '#/shared/lib/constants' +import { getServerEnv } from '#/shared/lib/server-env' + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + const token = typeof req.body?.token === 'string' ? req.body.token : null + if (!token) { + return res.status(400).json({ error: 'Token is required' }) + } + + const env = getServerEnv() + const clientId = env.NEXT_DJANGO_GOOGLE_APP_CLIENT_ID + const clientSecret = env.NEXT_DJANGO_GOOGLE_APP_CLIENT_SECRET + + if (!clientId || !clientSecret) { + return res.status(500).json({ error: 'Server configuration error' }) + } + + try { + const { data } = await axios.post(getApiUrl() + '/auth/login-social/convert-token', { + grant_type: 'convert_token', + client_id: clientId, + client_secret: clientSecret, + backend: 'google-oauth2', + token, + }) + + return res.status(200).json(data) + } catch { + return res.status(500).json({ error: 'Failed to convert token' }) + } +} @@ -0,0 +1,38 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import axios from 'axios' + +import { getServerEnv } from '#/shared/lib/server-env' + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + const query = typeof req.body?.query === 'string' ? req.body.query : null + if (!query) { + return res.status(400).json({ error: 'Query is required' }) + } + + const env = getServerEnv() + const url = env.NEXT_URL_DADATA + const token = env.NEXT_TOKEN_DADATA + + if (!url || !token) { + return res.status(500).json({ error: 'Server configuration error' }) + } + + try { + const { data } = await axios.post(url, JSON.stringify({ query }), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: 'Token ' + token, + }, + }) + + return res.status(200).json(data) + } catch { + return res.status(500).json({ error: 'Failed to fetch company data' }) + } +} @@ -1,13 +1,12 @@ import type { NextApiRequest, NextApiResponse } from 'next' - export default function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'GET') { return res.status(405).json({ error: 'Method not allowed' }) } const env = Object.entries(process.env) - .filter(([key]) => key.startsWith('NEXT')) + .filter(([key]) => key.startsWith('NEXT_PUBLIC')) .sort(([a], [b]) => a.localeCompare(b)) .reduce>((acc, [key, value]) => { acc[key] = value ?? '' @@ -1,12 +1,10 @@ import React, { ReactElement, ReactNode } from 'react' import { Provider } from 'react-redux' import { StyledEngineProvider, ThemeProvider } from '@mui/material' -import axios from 'axios' -import * as https from 'https' import { NextPage } from 'next' import type { AppProps } from 'next/app' import { Raleway } from 'next/font/google' -import { getSession,SessionProvider } from 'next-auth/react' +import { SessionProvider } from 'next-auth/react' import { appWithTranslation } from 'next-i18next' import { NextStep, NextStepProvider } from 'nextstepjs' import { FlagProvider } from '@unleash/proxy-client-react' @@ -20,39 +18,14 @@ import { useBlockTelegram } from '#/shared/lib/hooks/use-block-telegram' import { useEnv } from '#/shared/lib/hooks/use-env' import { Providers } from '#/widgets/providers' +import { setupAxios } from '#/shared/api/axios-interceptors' + import ErrorBoundary from './error-boundary' import '#/app/styles/globals.css' import '#/app/styles/styles-pages/system.scss' -axios.defaults.httpsAgent = new https.Agent({ - rejectUnauthorized: false, -}) - -axios.defaults.validateStatus = () => true - -axios.interceptors.response.use(async (response) => { - if (![401, 404, 403].includes(response.status)) return response - - if (typeof window === 'undefined') return response - - const session = await getSession() - - if (!session) { - window.location.href = '/login' - } - - return response -}) -axios.interceptors.response.use( - (response) => response, - (error) => { - if (!error.response && typeof window !== 'undefined') { - window.location.href = '/network-error' - } - return error - } -) +setupAxios() const inter = Raleway({ subsets: ['latin'] }) @@ -65,7 +38,15 @@ export interface AppPropsWithLayout extends AppProps { Component: NextPageWithLayout } -function AppContent({ Component, pageProps }: { Component: NextPageWithLayout; pageProps: any }) { +function AppContent({ + Component, + pageProps, + refetchInterval, +}: { + Component: NextPageWithLayout + pageProps: any + refetchInterval?: number +}) { const handleTourComplete = () => { setTourCompleted() } @@ -87,7 +68,7 @@ function AppContent({ Component, pageProps }: { Component: NextPageWithLayout; p - + @@ -109,7 +90,6 @@ function App({ Component, pageProps: { session, ...pageProps } }: AppPropsWithLa useBlockTelegram() const { env } = useEnv() - // Fallback на process.env для SSR/prerender — useEnv пустой до fetch const unleashUrl = env?.NEXT_PUBLIC_UNLEASH_URL || process.env.NEXT_PUBLIC_UNLEASH_URL || '' const unleashClientKey = env?.NEXT_PUBLIC_UNLEASH_CLIENT_KEY || process.env.NEXT_PUBLIC_UNLEASH_CLIENT_KEY || '' const unleashAppName = env?.NEXT_PUBLIC_UNLEASH_APP_NAME || process.env.NEXT_PUBLIC_UNLEASH_APP_NAME || '' @@ -125,7 +105,11 @@ function App({ Component, pageProps: { session, ...pageProps } }: AppPropsWithLa }} > - + @@ -0,0 +1,47 @@ +import axios from 'axios' +import * as https from 'https' +import { getSession, signOut } from 'next-auth/react' + +const BLACKLISTED_TOKEN_MESSAGE = 'Токен занесен в черный список' + +export function setupAxios() { + axios.defaults.httpsAgent = new https.Agent({ + rejectUnauthorized: false, + }) + + axios.defaults.validateStatus = () => true + + axios.interceptors.response.use(async (response) => { + if (typeof window === 'undefined') return response + + + if (response.status === 401) { + await signOut({ callbackUrl: '/login' }) + return response + } + + if (![401, 404, 403].includes(response.status)) return response + + const session = await getSession() + + if (!session || session.error) { + await signOut({ callbackUrl: '/login' }) + } + + return response + }) + + axios.interceptors.response.use( + (response) => response, + (error) => { + if (!error.response && typeof window !== 'undefined') { + window.location.href = '/network-error' + } + + if (error.response.status === 401) { + signOut({ callbackUrl: '/login' }) + } + return error + } + ) +} @@ -1,11 +1,11 @@ -import * as process from 'process' - import { getEnv } from '#/shared/lib/env-store' -export const getApiHost = () => - typeof window !== 'undefined' ? (getEnv()?.NEXT_PUBLIC_MAIN_URL ?? '') : (process.env.NEXT_PUBLIC_MAIN_URL ?? '') -export const getApiUrl = () => - typeof window !== 'undefined' ? (getEnv()?.NEXT_PUBLIC_API_HOST ?? '') : (process.env.NEXT_PUBLIC_API_HOST ?? '') +export const getApiHost = (): string => { + return getEnv()?.NEXT_PUBLIC_MAIN_URL ?? process.env.NEXT_PUBLIC_MAIN_URL ?? '' +} +export const getApiUrl = (): string => { + return getEnv()?.NEXT_PUBLIC_API_HOST ?? process.env.NEXT_PUBLIC_API_HOST ?? '' +} export enum ModelPagesList { '/chatgpt', @@ -1,20 +1,34 @@ import { useEffect, useState } from 'react' -import { setEnv } from '#/shared/lib/env-store' +import { setEnv as setEnvStore } from '#/shared/lib/env-store' +import { PublicEnv } from '../env-types' -type EnvRecord = Record const CACHE_TTL_MS = 5 * 60 * 1000 // 5 минут -let cache: EnvRecord | null = null +let cache: PublicEnv | null = null let cacheTimestamp = 0 -let fetchPromise: Promise | null = null +let fetchPromise: Promise | null = null function isCacheValid(): boolean { return cache !== null && Date.now() - cacheTimestamp < CACHE_TTL_MS } -function fetchEnv(): Promise { +function getProcessEnvFallback(): PublicEnv { + return { + NEXT_PUBLIC_MAIN_URL: process.env.NEXT_PUBLIC_MAIN_URL ?? '', + NEXT_PUBLIC_API_HOST: process.env.NEXT_PUBLIC_API_HOST ?? '', + NEXT_PUBLIC_SESSION_TIME: process.env.NEXT_PUBLIC_SESSION_TIME ?? '', + NEXT_PUBLIC_REFETCH_INTERVAL: process.env.NEXT_PUBLIC_REFETCH_INTERVAL ?? '', + NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN ?? '', + NEXT_PUBLIC_WS_API_URL: process.env.NEXT_PUBLIC_WS_API_URL ?? '', + NEXT_PUBLIC_UNLEASH_URL: process.env.NEXT_PUBLIC_UNLEASH_URL ?? '', + NEXT_PUBLIC_UNLEASH_CLIENT_KEY: process.env.NEXT_PUBLIC_UNLEASH_CLIENT_KEY ?? '', + NEXT_PUBLIC_UNLEASH_APP_NAME: process.env.NEXT_PUBLIC_UNLEASH_APP_NAME ?? '', + } +} + +function fetchEnv(): Promise { if (isCacheValid()) return Promise.resolve(cache!) if (fetchPromise) return fetchPromise @@ -23,7 +37,7 @@ function fetchEnv(): Promise { .then((data) => { cache = data cacheTimestamp = Date.now() - setEnv(data) + setEnvStore(data) return data }) .finally(() => { @@ -33,8 +47,8 @@ function fetchEnv(): Promise { return fetchPromise } -export function useEnv(): { env: EnvRecord | null; loading: boolean; error: Error | null } { - const [env, setEnv] = useState(isCacheValid() ? cache : null) +export function useEnv(): { env: PublicEnv | null; loading: boolean; error: Error | null } { + const [env, setEnv] = useState(isCacheValid() ? cache : null) const [loading, setLoading] = useState(!isCacheValid()) const [error, setError] = useState(null) @@ -48,7 +62,11 @@ export function useEnv(): { env: EnvRecord | null; loading: boolean; error: Erro fetchEnv() .then(setEnv) - .catch(setError) + .catch(() => { + const fallback = getProcessEnvFallback() + setEnvStore(fallback) + setEnv(fallback) + }) .finally(() => setLoading(false)) } @@ -1,11 +1,11 @@ -export type EnvRecord = Record +import { PublicEnv } from "./env-types" -let store: EnvRecord | null = null +let store: PublicEnv | null = null -export function getEnv(): EnvRecord | null { +export function getEnv(): PublicEnv | null { return store } -export function setEnv(env: EnvRecord): void { +export function setEnv(env: PublicEnv): void { store = env } @@ -0,0 +1,42 @@ +export interface PublicEnv { + NEXT_PUBLIC_MAIN_URL: string + NEXT_PUBLIC_API_HOST: string + NEXT_PUBLIC_SESSION_TIME: string + NEXT_PUBLIC_REFETCH_INTERVAL: string + NEXT_PUBLIC_SENTRY_DSN?: string + NEXT_PUBLIC_WS_API_URL?: string + + NEXT_PUBLIC_UNLEASH_URL?: string + NEXT_PUBLIC_UNLEASH_CLIENT_KEY?: string + NEXT_PUBLIC_UNLEASH_APP_NAME?: string +} + +export interface Env extends PublicEnv { + NEXTAUTH_URL: string + NEXTAUTH_SECRET: string + ANALYZE?: string + + NEXT_MAIN_URL: string + NEXT_API_HOST: string + NEXT_SESSION_TIME: string + NEXT_TOKEN_DADATA: string + NEXT_URL_DADATA: string + + NEXT_REDIRECT_URI_YANDEX: string + NEXT_REDIRECT_URI_GOOGLE: string + + NEXT_CLIENT_ID_GOOGLE: string + NEXT_CLIENT_SECRET_GOOGLE: string + NEXT_DJANGO_GOOGLE_APP_CLIENT_ID: string + NEXT_DJANGO_GOOGLE_APP_CLIENT_SECRET: string + + NEXT_CLIENT_ID_YANDEX: string + NEXT_CLIENT_SECRET_YANDEX: string + NEXT_DJANGO_YANDEX_APP_CLIENT_ID: string + NEXT_DJANGO_YANDEX_APP_CLIENT_SECRET: string + + NEXT_CLIENT_ID_VK: string + NEXT_CLIENT_SECRET_VK: string + NEXT_DJANGO_VK_APP_CLIENT_ID: string + NEXT_DJANGO_VK_APP_CLIENT_SECRET: string +} @@ -0,0 +1,5 @@ +import type { Env } from '#/shared/lib/env-types' + +export function getServerEnv(): Partial { + return process.env as Partial +} @@ -1,17 +1,8 @@ -import { getApiUrl } from '#/shared/lib/constants' -import axios from 'axios' - -import { getEnv } from '#/shared/lib/env-store' - export async function getTokenByAPIGoogle(token: string) { - const env = getEnv() - const { data } = await axios.post(getApiUrl() + '/auth/login-social/convert-token', { - grant_type: 'convert_token', - client_id: env?.NEXT_PUBLIC_DJANGO_GOOGLE_APP_CLIENT_ID, - client_secret: env?.NEXT_PUBLIC_DJANGO_GOOGLE_APP_CLIENT_SECRET, - backend: 'google-oauth2', - token, + const res = await fetch('/api/auth/convert-token-google', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token }), }) - - return data + return res.json() }