From 6a6a95e8630bd47ec0600e5dcf4e1aa93adbf909 Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Mon, 28 Sep 2026 09:56:59 +0300 Subject: [PATCH 01/10] tests for temp_password --- src/service/users_crud/users_crud.py | 4 ++++ tests/unit/test_jwt.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/service/users_crud/users_crud.py b/src/service/users_crud/users_crud.py index d187bea..dbf2c89 100644 --- a/src/service/users_crud/users_crud.py +++ b/src/service/users_crud/users_crud.py @@ -1,5 +1,6 @@ import asyncio +from src.database.auth.refresh_tokens import JwtCrudActions from src.database.users.crud import UsersCrudActions from src.errors.http_errors.errors import Errors from src.models.pydantic_models.model import UserCreate, UserOut, UserUpdate @@ -12,6 +13,7 @@ class CrudService: self.errors=Errors() self.crud_db_actions=UsersCrudActions() self.hash_service=HashService() + self.jwt_db_actions=JwtCrudActions() async def _plain_to_hash(self, user_data:dict)->dict: @@ -78,6 +80,8 @@ class CrudService: async def reset_password(self, email:str)->str: new_password=await self.hash_service.generate_temp_password() + + await self.update_user(email, UserUpdate(plain_password=new_password)) # type: ignore[call-arg] return new_password diff --git a/tests/unit/test_jwt.py b/tests/unit/test_jwt.py index 614d3f0..47f145e 100644 --- a/tests/unit/test_jwt.py +++ b/tests/unit/test_jwt.py @@ -6,6 +6,7 @@ from fastapi import HTTPException from jose import jwt from src.models.configs_read.env import env_settings +from src.models.pydantic_models.model import validate_password from src.service.auth.jwt import HashService, JwtService @@ -146,3 +147,30 @@ class TestJwt: async def test_token_to_hash_different_input(self, hash_service:HashService)->None: assert hash_service.token_to_hash("abc")!=hash_service.token_to_hash("xyz") + + @pytest.mark.parametrize("complexity",[ + pytest.param(8,id="low_edge"), + pytest.param(9,id="low_edge+1"), + pytest.param(16,id="default"), + pytest.param(72,id="high_edge"), + pytest.param(71,id="high_edge-1") + ]) + async def test_generate_temp_password_positive(self, hash_service:HashService, complexity:int)->None: + + for _ in range(10000): + password=await hash_service.generate_temp_password(complexity) + try: + validate_password(password) + except ValueError as e: + pytest.fail(f"{password!r}: {e}") + + + @pytest.mark.parametrize("complexity", [ + pytest.param(73,id="too_long"), + pytest.param(7,id="too_short") + ]) + async def test_generate_temp_password_negative(self, hash_service:HashService, complexity:int)->None: + + password = await hash_service.generate_temp_password(complexity) + with pytest.raises(ValueError, match="8-72"): + validate_password(password) \ No newline at end of file From bcfd4acfade8fd496f7177b4e3b0e08f4e213eee Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Wed, 30 Sep 2026 14:33:20 +0300 Subject: [PATCH 02/10] save tokens localstorage, fix refresh sql query, added permissions --- frontend/src/App.css | 4 -- frontend/src/App.tsx | 8 ++- frontend/src/api/LoginAPI/Auth.ts | 10 ++- frontend/src/api/UserAPI/User.ts | 9 +++ frontend/src/api/token.ts | 52 +++++++++++++- .../components/RedirectIfAuthenticated.tsx | 12 ++++ frontend/src/components/RequirePermission.tsx | 13 ++++ frontend/src/context/AuthContext.ts | 12 ++++ frontend/src/context/AuthProvider.tsx | 67 +++++++++++++++++++ .../auth/components/AuthComponent.tsx | 10 ++- frontend/src/hooks/useAuth.ts | 8 +++ frontend/src/layouts/AuthLayout.css | 3 + frontend/src/main.tsx | 5 +- frontend/src/pages/DashboardPage.tsx | 22 ++++++ frontend/src/utils/permissions/Permissions.ts | 8 +++ frontend/src/utils/rememberMe.ts | 13 ++++ src/database/auth/refresh_tokens.py | 15 +++-- src/web/protected_routes/auth_routes.py | 6 +- 18 files changed, 255 insertions(+), 22 deletions(-) create mode 100644 frontend/src/api/UserAPI/User.ts create mode 100644 frontend/src/components/RedirectIfAuthenticated.tsx create mode 100644 frontend/src/components/RequirePermission.tsx create mode 100644 frontend/src/context/AuthContext.ts create mode 100644 frontend/src/context/AuthProvider.tsx create mode 100644 frontend/src/hooks/useAuth.ts create mode 100644 frontend/src/pages/DashboardPage.tsx create mode 100644 frontend/src/utils/permissions/Permissions.ts create mode 100644 frontend/src/utils/rememberMe.ts diff --git a/frontend/src/App.css b/frontend/src/App.css index 3fc8113..ba3535a 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,7 +1,3 @@ #root { height: 100%; - box-shadow: 1px 1px 20px 1px rgba(68, 68, 68, 0.5); - border-radius: 40px; - border: 1px solid #444; - overflow: hidden; } \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 411f6b1..5607d28 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,12 +2,16 @@ import './App.css' import { Route, Routes } from "react-router-dom"; import { LoginPage } from "./pages/LoginPage"; import { ForgotPasswordPage } from "./pages/ForgotPasswordPage"; +import { DashboardPage } from "./pages/DashboardPage"; +import { RequirePermission } from "./components/RequirePermission"; +import { RedirectIfAuthenticated } from "./components/RedirectIfAuthenticated"; function App() { return ( - } /> - } /> + } /> + } /> + } /> ); } diff --git a/frontend/src/api/LoginAPI/Auth.ts b/frontend/src/api/LoginAPI/Auth.ts index 97935c9..d4c87ea 100644 --- a/frontend/src/api/LoginAPI/Auth.ts +++ b/frontend/src/api/LoginAPI/Auth.ts @@ -1,14 +1,18 @@ -import { API_URL, setAccessToken } from "../token"; +import { API_URL, authFetch, setAccessToken } from "../token"; export async function login(email: string, password: string) { const body = new URLSearchParams({ username: email, password }); const res = await fetch(`${API_URL}/protected/token`, { method: "POST", - body, - credentials: "include", + body, + credentials: "include", }); if (!res.ok) throw new Error("Неверный email или пароль"); const data = await res.json(); setAccessToken(data.access_token); return data; } + +export async function logout() { + return authFetch("/protected/logout"); +} diff --git a/frontend/src/api/UserAPI/User.ts b/frontend/src/api/UserAPI/User.ts new file mode 100644 index 0000000..5b45847 --- /dev/null +++ b/frontend/src/api/UserAPI/User.ts @@ -0,0 +1,9 @@ +import type { UserOut } from "../../types/UserTypes"; +import { authFetch } from "../token"; + + +export async function fetchCurrentUser(): Promise { + const res = await authFetch("/protected/me"); + if (!res.ok) throw new Error("Не удалось получить данные пользователя"); + return res.json(); +} \ No newline at end of file diff --git a/frontend/src/api/token.ts b/frontend/src/api/token.ts index 14bcfce..439a0b1 100644 --- a/frontend/src/api/token.ts +++ b/frontend/src/api/token.ts @@ -10,8 +10,58 @@ export function getAccessToken() { return accessToken; } +async function requestRefresh(): Promise<{ access_token: string } | null> { + const res = await fetch(`${API_URL}/protected/refresh`, { + method: "POST", + credentials: "include", + }); + if (!res.ok) return null; + return res.json(); +} + +let refreshInFlight: Promise | null = null; + +export function refreshAccessToken(): Promise { + if (refreshInFlight) return refreshInFlight; + + refreshInFlight = (async () => { + try { + // refresh_token в cookie одноразовый (ротация): если другая вкладка + // или повторный вызов уже успели обменять его на новый ровно перед + // нашим запросом, наш "старый" токен уже отозван и сервер ответит + // 404/401, хотя сессия на самом деле жива. Одна короткая повторная + // попытка подхватит уже обновлённую браузером cookie и не даст + // разлогинить валидного пользователя из-за проигранной гонки. + let data = await requestRefresh(); + if (!data) { + await new Promise((resolve) => setTimeout(resolve, 300)); + data = await requestRefresh(); + } + if (!data) { + setAccessToken(null); + return false; + } + setAccessToken(data.access_token); + return true; + } finally { + refreshInFlight = null; + } + })(); + + return refreshInFlight; +} + export async function authFetch(path: string, init: RequestInit = {}) { const headers = new Headers(init.headers); if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`); - return fetch(`${API_URL}${path}`, { ...init, headers, credentials: "include" }); + + const res = await fetch(`${API_URL}${path}`, { ...init, headers, credentials: "include" }); + if (res.status !== 401) return res; + + const refreshed = await refreshAccessToken(); + if (!refreshed) return res; + + const retryHeaders = new Headers(init.headers); + if (accessToken) retryHeaders.set("Authorization", `Bearer ${accessToken}`); + return fetch(`${API_URL}${path}`, { ...init, headers: retryHeaders, credentials: "include" }); } diff --git a/frontend/src/components/RedirectIfAuthenticated.tsx b/frontend/src/components/RedirectIfAuthenticated.tsx new file mode 100644 index 0000000..695cc82 --- /dev/null +++ b/frontend/src/components/RedirectIfAuthenticated.tsx @@ -0,0 +1,12 @@ +import type { ReactNode } from "react"; +import { Navigate } from "react-router-dom"; +import { useAuth } from "../hooks/useAuth"; + +export function RedirectIfAuthenticated({ children }: { children: ReactNode }) { + const { user, loading } = useAuth(); + + if (loading) return null; + if (user) return ; + + return children; +} diff --git a/frontend/src/components/RequirePermission.tsx b/frontend/src/components/RequirePermission.tsx new file mode 100644 index 0000000..b023157 --- /dev/null +++ b/frontend/src/components/RequirePermission.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from "react"; +import { Navigate } from "react-router-dom"; +import { useAuth } from "../hooks/useAuth"; + +export function RequirePermission({ permission, children }: { permission?: string; children: ReactNode }) { + const { user, permissions, loading } = useAuth(); + + if (loading) return null; + if (!user) return ; + if (permission && !permissions.has(permission)) return ; + + return children; +} diff --git a/frontend/src/context/AuthContext.ts b/frontend/src/context/AuthContext.ts new file mode 100644 index 0000000..2137587 --- /dev/null +++ b/frontend/src/context/AuthContext.ts @@ -0,0 +1,12 @@ +import { createContext } from "react"; +import type { UserOut } from "../types/UserTypes"; + +export type AuthState = { + user: UserOut | null; + permissions: Set; + loading: boolean; + refreshUser: () => Promise>; + logout: () => Promise; +}; + +export const AuthContext = createContext(null); diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx new file mode 100644 index 0000000..00b8e97 --- /dev/null +++ b/frontend/src/context/AuthProvider.tsx @@ -0,0 +1,67 @@ +import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; +import { fetchCurrentUser } from "../api/UserAPI/User"; +import { logout as apiLogout } from "../api/LoginAPI/Auth"; +import { refreshAccessToken, setAccessToken } from "../api/token"; +import { getEffectivePermissions } from "../utils/permissions/Permissions"; +import { getRememberMe, setRememberMe } from "../utils/rememberMe"; +import { AuthContext, type AuthState } from "./AuthContext"; + +export function AuthProvider({ children }: { children: ReactNode }) { + const [state, setState] = useState>({ + user: null, + permissions: new Set(), + loading: true, + }); + + const refreshUser = useCallback(async () => { + setState((s) => ({ ...s, loading: true })); + try { + const user = await fetchCurrentUser(); + const permissions = getEffectivePermissions(user); + setState({ user, permissions, loading: false }); + return permissions; + } catch { + setState({ user: null, permissions: new Set(), loading: false }); + return new Set(); + } + }, []); + + const logout = useCallback(async () => { + try { + await apiLogout(); + } catch { + // ошибку сети/сервера игнорируем — состояние всё равно чистим ниже + } finally { + setAccessToken(null); + setRememberMe(false); + setState({ user: null, permissions: new Set(), loading: false }); + } + }, []); + + const started = useRef(false); + + useEffect(() => { + if (started.current) return; + started.current = true; + + if (!getRememberMe()) { + setState({ user: null, permissions: new Set(), loading: false }); + return; + } + + (async () => { + const ok = await refreshAccessToken(); + if (ok) { + await refreshUser(); + } else { + setState({ user: null, permissions: new Set(), loading: false }); + } + })(); + }, [refreshUser]); + + return ( + + {children} + + ); +} diff --git a/frontend/src/features/auth/components/AuthComponent.tsx b/frontend/src/features/auth/components/AuthComponent.tsx index cfbd8f7..6942d5d 100644 --- a/frontend/src/features/auth/components/AuthComponent.tsx +++ b/frontend/src/features/auth/components/AuthComponent.tsx @@ -1,9 +1,13 @@ import { useState, type SubmitEvent, type ReactElement } from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import styles from "./AuthComponent.module.css"; import { login } from "../../../api/LoginAPI/Auth"; +import { useAuth } from "../../../hooks/useAuth"; +import { setRememberMe as persistRememberMe } from "../../../utils/rememberMe"; export function Auth():ReactElement { + const { refreshUser } = useAuth(); + const navigate = useNavigate(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [rememberMe, setRememberMe] = useState(false); @@ -26,7 +30,9 @@ export function Auth():ReactElement { try { await login(email, password); - window.location.href = "http://localhost:8000/docs"; + persistRememberMe(rememberMe); + await refreshUser(); + navigate("/dashboard"); } catch (err) { console.error(err); setErrors({ password: "Неверный email или пароль" }); diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts new file mode 100644 index 0000000..504d97f --- /dev/null +++ b/frontend/src/hooks/useAuth.ts @@ -0,0 +1,8 @@ +import { useContext } from "react"; +import { AuthContext } from "../context/AuthContext"; + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("useAuth must be used inside AuthProvider"); + return ctx; +} diff --git a/frontend/src/layouts/AuthLayout.css b/frontend/src/layouts/AuthLayout.css index 1cfbfec..60bdf9e 100644 --- a/frontend/src/layouts/AuthLayout.css +++ b/frontend/src/layouts/AuthLayout.css @@ -5,6 +5,9 @@ width: 100%; overflow: hidden; height: 100%; + box-shadow: 1px 1px 20px 1px rgba(68, 68, 68, 0.5); + border-radius: 40px; + border: 1px solid #444; } .left_side { width: 60%; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index ade9d64..24269be 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,11 +3,14 @@ import { createRoot } from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' import './index.css' import App from './App.tsx' +import { AuthProvider } from './context/AuthProvider.tsx' createRoot(document.getElementById('root')!).render( - + + + , ) diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..bce1fff --- /dev/null +++ b/frontend/src/pages/DashboardPage.tsx @@ -0,0 +1,22 @@ +import { useAuth } from "../hooks/useAuth"; + +export function DashboardPage() { + const { permissions, logout } = useAuth(); + const isAdmin = permissions.has("admin"); + + return ( +
+
+

User

+
+ + + + {isAdmin && ( +
+

Admin

+
+ )} +
+ ); +} diff --git a/frontend/src/utils/permissions/Permissions.ts b/frontend/src/utils/permissions/Permissions.ts new file mode 100644 index 0000000..f2ae736 --- /dev/null +++ b/frontend/src/utils/permissions/Permissions.ts @@ -0,0 +1,8 @@ +import type { UserOut } from "../../types/UserTypes"; + + +export function getEffectivePermissions(user: UserOut): Set { + const direct = user.direct_permissions.map(p => p.permission); + const fromGroups = user.group.flatMap(g => g.permissions.map(p => p.permission)); + return new Set([...direct, ...fromGroups]); +} \ No newline at end of file diff --git a/frontend/src/utils/rememberMe.ts b/frontend/src/utils/rememberMe.ts new file mode 100644 index 0000000..fa8ef2f --- /dev/null +++ b/frontend/src/utils/rememberMe.ts @@ -0,0 +1,13 @@ +const REMEMBER_ME_KEY = "rememberMe"; + +export function setRememberMe(value: boolean) { + if (value) { + localStorage.setItem(REMEMBER_ME_KEY, "1"); + } else { + localStorage.removeItem(REMEMBER_ME_KEY); + } +} + +export function getRememberMe(): boolean { + return localStorage.getItem(REMEMBER_ME_KEY) === "1"; +} diff --git a/src/database/auth/refresh_tokens.py b/src/database/auth/refresh_tokens.py index b74f624..fadfc0e 100644 --- a/src/database/auth/refresh_tokens.py +++ b/src/database/auth/refresh_tokens.py @@ -36,20 +36,23 @@ class JwtCrudActions: async def create_and_update_token(self, data: dict, old_jti: UUID, new_jti: UUID) -> bool: async with self.Session() as session, session.begin(): - new_token = RefreshTokens(**data) - - query = ( + revoke_query = ( update(RefreshTokens) .where(RefreshTokens.id == old_jti, RefreshTokens.is_revoked.is_(False)) - .values(is_revoked=True, replaced_by=new_jti) + .values(is_revoked=True) .returning(RefreshTokens.id) ) - updated_id = (await session.execute(query)).scalar_one_or_none() + updated_id = (await session.execute(revoke_query)).scalar_one_or_none() if updated_id is None: return False - session.add(new_token) + session.add(RefreshTokens(**data)) + await session.flush() + + await session.execute( + update(RefreshTokens).where(RefreshTokens.id == old_jti).values(replaced_by=new_jti) + ) return True diff --git a/src/web/protected_routes/auth_routes.py b/src/web/protected_routes/auth_routes.py index 64c4809..7b7603e 100644 --- a/src/web/protected_routes/auth_routes.py +++ b/src/web/protected_routes/auth_routes.py @@ -93,6 +93,6 @@ async def reset(request: Request, return {"detail":"New password was sent"} -@router.get("") -async def protected(current_user:UserOut=Depends(require_permissions()))->dict: - return {"protected router": "Hello, this is a protected router"} +@router.get("/me") +async def protected(current_user:UserOut=Depends(require_permissions()))->UserOut: + return current_user From 1fbec06c232ec01fc56e1d2febe6ef802be7b7fc Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Wed, 30 Sep 2026 16:18:36 +0300 Subject: [PATCH 03/10] revert the front --- frontend/src/App.css | 4 ++ frontend/src/App.tsx | 8 +-- frontend/src/api/LoginAPI/Auth.ts | 10 +-- frontend/src/api/UserAPI/User.ts | 9 --- frontend/src/api/token.ts | 52 +------------- .../components/RedirectIfAuthenticated.tsx | 12 ---- frontend/src/components/RequirePermission.tsx | 13 ---- frontend/src/context/AuthContext.ts | 12 ---- frontend/src/context/AuthProvider.tsx | 67 ------------------- .../auth/components/AuthComponent.tsx | 10 +-- frontend/src/hooks/useAuth.ts | 8 --- frontend/src/layouts/AuthLayout.css | 3 - frontend/src/main.tsx | 5 +- frontend/src/pages/DashboardPage.tsx | 22 ------ frontend/src/utils/permissions/Permissions.ts | 8 --- frontend/src/utils/rememberMe.ts | 13 ---- 16 files changed, 13 insertions(+), 243 deletions(-) delete mode 100644 frontend/src/api/UserAPI/User.ts delete mode 100644 frontend/src/components/RedirectIfAuthenticated.tsx delete mode 100644 frontend/src/components/RequirePermission.tsx delete mode 100644 frontend/src/context/AuthContext.ts delete mode 100644 frontend/src/context/AuthProvider.tsx delete mode 100644 frontend/src/hooks/useAuth.ts delete mode 100644 frontend/src/pages/DashboardPage.tsx delete mode 100644 frontend/src/utils/permissions/Permissions.ts delete mode 100644 frontend/src/utils/rememberMe.ts diff --git a/frontend/src/App.css b/frontend/src/App.css index ba3535a..3fc8113 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,3 +1,7 @@ #root { height: 100%; + box-shadow: 1px 1px 20px 1px rgba(68, 68, 68, 0.5); + border-radius: 40px; + border: 1px solid #444; + overflow: hidden; } \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5607d28..411f6b1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,16 +2,12 @@ import './App.css' import { Route, Routes } from "react-router-dom"; import { LoginPage } from "./pages/LoginPage"; import { ForgotPasswordPage } from "./pages/ForgotPasswordPage"; -import { DashboardPage } from "./pages/DashboardPage"; -import { RequirePermission } from "./components/RequirePermission"; -import { RedirectIfAuthenticated } from "./components/RedirectIfAuthenticated"; function App() { return ( - } /> - } /> - } /> + } /> + } /> ); } diff --git a/frontend/src/api/LoginAPI/Auth.ts b/frontend/src/api/LoginAPI/Auth.ts index d4c87ea..97935c9 100644 --- a/frontend/src/api/LoginAPI/Auth.ts +++ b/frontend/src/api/LoginAPI/Auth.ts @@ -1,18 +1,14 @@ -import { API_URL, authFetch, setAccessToken } from "../token"; +import { API_URL, setAccessToken } from "../token"; export async function login(email: string, password: string) { const body = new URLSearchParams({ username: email, password }); const res = await fetch(`${API_URL}/protected/token`, { method: "POST", - body, - credentials: "include", + body, + credentials: "include", }); if (!res.ok) throw new Error("Неверный email или пароль"); const data = await res.json(); setAccessToken(data.access_token); return data; } - -export async function logout() { - return authFetch("/protected/logout"); -} diff --git a/frontend/src/api/UserAPI/User.ts b/frontend/src/api/UserAPI/User.ts deleted file mode 100644 index 5b45847..0000000 --- a/frontend/src/api/UserAPI/User.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { UserOut } from "../../types/UserTypes"; -import { authFetch } from "../token"; - - -export async function fetchCurrentUser(): Promise { - const res = await authFetch("/protected/me"); - if (!res.ok) throw new Error("Не удалось получить данные пользователя"); - return res.json(); -} \ No newline at end of file diff --git a/frontend/src/api/token.ts b/frontend/src/api/token.ts index 439a0b1..14bcfce 100644 --- a/frontend/src/api/token.ts +++ b/frontend/src/api/token.ts @@ -10,58 +10,8 @@ export function getAccessToken() { return accessToken; } -async function requestRefresh(): Promise<{ access_token: string } | null> { - const res = await fetch(`${API_URL}/protected/refresh`, { - method: "POST", - credentials: "include", - }); - if (!res.ok) return null; - return res.json(); -} - -let refreshInFlight: Promise | null = null; - -export function refreshAccessToken(): Promise { - if (refreshInFlight) return refreshInFlight; - - refreshInFlight = (async () => { - try { - // refresh_token в cookie одноразовый (ротация): если другая вкладка - // или повторный вызов уже успели обменять его на новый ровно перед - // нашим запросом, наш "старый" токен уже отозван и сервер ответит - // 404/401, хотя сессия на самом деле жива. Одна короткая повторная - // попытка подхватит уже обновлённую браузером cookie и не даст - // разлогинить валидного пользователя из-за проигранной гонки. - let data = await requestRefresh(); - if (!data) { - await new Promise((resolve) => setTimeout(resolve, 300)); - data = await requestRefresh(); - } - if (!data) { - setAccessToken(null); - return false; - } - setAccessToken(data.access_token); - return true; - } finally { - refreshInFlight = null; - } - })(); - - return refreshInFlight; -} - export async function authFetch(path: string, init: RequestInit = {}) { const headers = new Headers(init.headers); if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`); - - const res = await fetch(`${API_URL}${path}`, { ...init, headers, credentials: "include" }); - if (res.status !== 401) return res; - - const refreshed = await refreshAccessToken(); - if (!refreshed) return res; - - const retryHeaders = new Headers(init.headers); - if (accessToken) retryHeaders.set("Authorization", `Bearer ${accessToken}`); - return fetch(`${API_URL}${path}`, { ...init, headers: retryHeaders, credentials: "include" }); + return fetch(`${API_URL}${path}`, { ...init, headers, credentials: "include" }); } diff --git a/frontend/src/components/RedirectIfAuthenticated.tsx b/frontend/src/components/RedirectIfAuthenticated.tsx deleted file mode 100644 index 695cc82..0000000 --- a/frontend/src/components/RedirectIfAuthenticated.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { ReactNode } from "react"; -import { Navigate } from "react-router-dom"; -import { useAuth } from "../hooks/useAuth"; - -export function RedirectIfAuthenticated({ children }: { children: ReactNode }) { - const { user, loading } = useAuth(); - - if (loading) return null; - if (user) return ; - - return children; -} diff --git a/frontend/src/components/RequirePermission.tsx b/frontend/src/components/RequirePermission.tsx deleted file mode 100644 index b023157..0000000 --- a/frontend/src/components/RequirePermission.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { ReactNode } from "react"; -import { Navigate } from "react-router-dom"; -import { useAuth } from "../hooks/useAuth"; - -export function RequirePermission({ permission, children }: { permission?: string; children: ReactNode }) { - const { user, permissions, loading } = useAuth(); - - if (loading) return null; - if (!user) return ; - if (permission && !permissions.has(permission)) return ; - - return children; -} diff --git a/frontend/src/context/AuthContext.ts b/frontend/src/context/AuthContext.ts deleted file mode 100644 index 2137587..0000000 --- a/frontend/src/context/AuthContext.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createContext } from "react"; -import type { UserOut } from "../types/UserTypes"; - -export type AuthState = { - user: UserOut | null; - permissions: Set; - loading: boolean; - refreshUser: () => Promise>; - logout: () => Promise; -}; - -export const AuthContext = createContext(null); diff --git a/frontend/src/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx deleted file mode 100644 index 00b8e97..0000000 --- a/frontend/src/context/AuthProvider.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; -import { fetchCurrentUser } from "../api/UserAPI/User"; -import { logout as apiLogout } from "../api/LoginAPI/Auth"; -import { refreshAccessToken, setAccessToken } from "../api/token"; -import { getEffectivePermissions } from "../utils/permissions/Permissions"; -import { getRememberMe, setRememberMe } from "../utils/rememberMe"; -import { AuthContext, type AuthState } from "./AuthContext"; - -export function AuthProvider({ children }: { children: ReactNode }) { - const [state, setState] = useState>({ - user: null, - permissions: new Set(), - loading: true, - }); - - const refreshUser = useCallback(async () => { - setState((s) => ({ ...s, loading: true })); - try { - const user = await fetchCurrentUser(); - const permissions = getEffectivePermissions(user); - setState({ user, permissions, loading: false }); - return permissions; - } catch { - setState({ user: null, permissions: new Set(), loading: false }); - return new Set(); - } - }, []); - - const logout = useCallback(async () => { - try { - await apiLogout(); - } catch { - // ошибку сети/сервера игнорируем — состояние всё равно чистим ниже - } finally { - setAccessToken(null); - setRememberMe(false); - setState({ user: null, permissions: new Set(), loading: false }); - } - }, []); - - const started = useRef(false); - - useEffect(() => { - if (started.current) return; - started.current = true; - - if (!getRememberMe()) { - setState({ user: null, permissions: new Set(), loading: false }); - return; - } - - (async () => { - const ok = await refreshAccessToken(); - if (ok) { - await refreshUser(); - } else { - setState({ user: null, permissions: new Set(), loading: false }); - } - })(); - }, [refreshUser]); - - return ( - - {children} - - ); -} diff --git a/frontend/src/features/auth/components/AuthComponent.tsx b/frontend/src/features/auth/components/AuthComponent.tsx index 6942d5d..cfbd8f7 100644 --- a/frontend/src/features/auth/components/AuthComponent.tsx +++ b/frontend/src/features/auth/components/AuthComponent.tsx @@ -1,13 +1,9 @@ import { useState, type SubmitEvent, type ReactElement } from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { Link } from "react-router-dom"; import styles from "./AuthComponent.module.css"; import { login } from "../../../api/LoginAPI/Auth"; -import { useAuth } from "../../../hooks/useAuth"; -import { setRememberMe as persistRememberMe } from "../../../utils/rememberMe"; export function Auth():ReactElement { - const { refreshUser } = useAuth(); - const navigate = useNavigate(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [rememberMe, setRememberMe] = useState(false); @@ -30,9 +26,7 @@ export function Auth():ReactElement { try { await login(email, password); - persistRememberMe(rememberMe); - await refreshUser(); - navigate("/dashboard"); + window.location.href = "http://localhost:8000/docs"; } catch (err) { console.error(err); setErrors({ password: "Неверный email или пароль" }); diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts deleted file mode 100644 index 504d97f..0000000 --- a/frontend/src/hooks/useAuth.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useContext } from "react"; -import { AuthContext } from "../context/AuthContext"; - -export function useAuth() { - const ctx = useContext(AuthContext); - if (!ctx) throw new Error("useAuth must be used inside AuthProvider"); - return ctx; -} diff --git a/frontend/src/layouts/AuthLayout.css b/frontend/src/layouts/AuthLayout.css index 60bdf9e..1cfbfec 100644 --- a/frontend/src/layouts/AuthLayout.css +++ b/frontend/src/layouts/AuthLayout.css @@ -5,9 +5,6 @@ width: 100%; overflow: hidden; height: 100%; - box-shadow: 1px 1px 20px 1px rgba(68, 68, 68, 0.5); - border-radius: 40px; - border: 1px solid #444; } .left_side { width: 60%; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 24269be..ade9d64 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,14 +3,11 @@ import { createRoot } from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' import './index.css' import App from './App.tsx' -import { AuthProvider } from './context/AuthProvider.tsx' createRoot(document.getElementById('root')!).render( - - - + , ) diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx deleted file mode 100644 index bce1fff..0000000 --- a/frontend/src/pages/DashboardPage.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useAuth } from "../hooks/useAuth"; - -export function DashboardPage() { - const { permissions, logout } = useAuth(); - const isAdmin = permissions.has("admin"); - - return ( -
-
-

User

-
- - - - {isAdmin && ( -
-

Admin

-
- )} -
- ); -} diff --git a/frontend/src/utils/permissions/Permissions.ts b/frontend/src/utils/permissions/Permissions.ts deleted file mode 100644 index f2ae736..0000000 --- a/frontend/src/utils/permissions/Permissions.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { UserOut } from "../../types/UserTypes"; - - -export function getEffectivePermissions(user: UserOut): Set { - const direct = user.direct_permissions.map(p => p.permission); - const fromGroups = user.group.flatMap(g => g.permissions.map(p => p.permission)); - return new Set([...direct, ...fromGroups]); -} \ No newline at end of file diff --git a/frontend/src/utils/rememberMe.ts b/frontend/src/utils/rememberMe.ts deleted file mode 100644 index fa8ef2f..0000000 --- a/frontend/src/utils/rememberMe.ts +++ /dev/null @@ -1,13 +0,0 @@ -const REMEMBER_ME_KEY = "rememberMe"; - -export function setRememberMe(value: boolean) { - if (value) { - localStorage.setItem(REMEMBER_ME_KEY, "1"); - } else { - localStorage.removeItem(REMEMBER_ME_KEY); - } -} - -export function getRememberMe(): boolean { - return localStorage.getItem(REMEMBER_ME_KEY) === "1"; -} From af151f254b329923b12a445f3116b6740f7f0d57 Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Thu, 1 Oct 2026 10:45:51 +0300 Subject: [PATCH 04/10] fix layout for dashboard, add dashboard, save rememberme state in localstorage --- frontend/src/App.css | 4 ---- frontend/src/App.tsx | 2 ++ frontend/src/features/auth/components/AuthComponent.tsx | 9 ++++++--- frontend/src/layouts/AuthLayout.css | 3 +++ frontend/src/pages/Dashboard.tsx | 9 +++++++++ frontend/src/utils/rememberMe.ts | 9 +++++++++ 6 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 frontend/src/pages/Dashboard.tsx create mode 100644 frontend/src/utils/rememberMe.ts diff --git a/frontend/src/App.css b/frontend/src/App.css index 3fc8113..ba3535a 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,7 +1,3 @@ #root { height: 100%; - box-shadow: 1px 1px 20px 1px rgba(68, 68, 68, 0.5); - border-radius: 40px; - border: 1px solid #444; - overflow: hidden; } \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 411f6b1..f1abb1b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,12 +2,14 @@ import './App.css' import { Route, Routes } from "react-router-dom"; import { LoginPage } from "./pages/LoginPage"; import { ForgotPasswordPage } from "./pages/ForgotPasswordPage"; +import { DashboardPage } from './pages/Dashboard'; function App() { return ( } /> } /> + } /> ); } diff --git a/frontend/src/features/auth/components/AuthComponent.tsx b/frontend/src/features/auth/components/AuthComponent.tsx index cfbd8f7..f36d79b 100644 --- a/frontend/src/features/auth/components/AuthComponent.tsx +++ b/frontend/src/features/auth/components/AuthComponent.tsx @@ -1,7 +1,8 @@ import { useState, type SubmitEvent, type ReactElement } from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import styles from "./AuthComponent.module.css"; import { login } from "../../../api/LoginAPI/Auth"; +import { persistRememberMe } from "../../../utils/rememberMe"; export function Auth():ReactElement { const [email, setEmail] = useState(""); @@ -9,6 +10,7 @@ export function Auth():ReactElement { const [rememberMe, setRememberMe] = useState(false); const [showPassword, setShowPassword] = useState(false); const [errors, setErrors] = useState<{ email?: string; password?: string }>({}); + const navigate = useNavigate(); const validateEmail = (value: string) => { if (!value) return "Введите email"; @@ -26,12 +28,13 @@ export function Auth():ReactElement { try { await login(email, password); - window.location.href = "http://localhost:8000/docs"; + persistRememberMe(rememberMe); + navigate("/dashboard"); } catch (err) { console.error(err); setErrors({ password: "Неверный email или пароль" }); } - }; + }; return ( <> diff --git a/frontend/src/layouts/AuthLayout.css b/frontend/src/layouts/AuthLayout.css index 1cfbfec..60bdf9e 100644 --- a/frontend/src/layouts/AuthLayout.css +++ b/frontend/src/layouts/AuthLayout.css @@ -5,6 +5,9 @@ width: 100%; overflow: hidden; height: 100%; + box-shadow: 1px 1px 20px 1px rgba(68, 68, 68, 0.5); + border-radius: 40px; + border: 1px solid #444; } .left_side { width: 60%; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..ae80c62 --- /dev/null +++ b/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,9 @@ + + +export function DashboardPage() { + return ( + <> +

test

+ + ); +} \ No newline at end of file diff --git a/frontend/src/utils/rememberMe.ts b/frontend/src/utils/rememberMe.ts new file mode 100644 index 0000000..839b2d5 --- /dev/null +++ b/frontend/src/utils/rememberMe.ts @@ -0,0 +1,9 @@ + + +export function persistRememberMe(value:boolean):void { + if (value) { + localStorage.setItem("rememberMe", "True"); + } else{ + localStorage.removeItem("rememberMe"); + } +}; \ No newline at end of file From e99aafa36a9a398486df49dfa993df6dc29b74de Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Fri, 2 Oct 2026 17:17:54 +0300 Subject: [PATCH 05/10] rememberme, dashboard, logout features --- frontend/src/api/LoginAPI/Auth.ts | 4 +- frontend/src/api/RefreshAPI/Refresh.ts | 13 ++++++ frontend/src/api/ResetAPI/Reset.ts | 4 +- frontend/src/api/UserAPI/User.ts | 13 ++++++ frontend/src/api/token.ts | 38 ++++++++++++++-- .../auth/components/AuthComponent.tsx | 35 +++++++-------- .../auth/components/ResetComponent.tsx | 4 +- frontend/src/main.tsx | 10 +++++ frontend/src/pages/Dashboard.tsx | 43 ++++++++++++++++++- frontend/src/pages/LoginPage.tsx | 29 +++++++++++++ frontend/src/utils/rememberMe.ts | 14 +++++- 11 files changed, 181 insertions(+), 26 deletions(-) create mode 100644 frontend/src/api/RefreshAPI/Refresh.ts create mode 100644 frontend/src/api/UserAPI/User.ts diff --git a/frontend/src/api/LoginAPI/Auth.ts b/frontend/src/api/LoginAPI/Auth.ts index 97935c9..9db04f4 100644 --- a/frontend/src/api/LoginAPI/Auth.ts +++ b/frontend/src/api/LoginAPI/Auth.ts @@ -7,7 +7,9 @@ export async function login(email: string, password: string) { body, credentials: "include", }); - if (!res.ok) throw new Error("Неверный email или пароль"); + if (!res.ok){ + throw new Error("Неверный email или пароль"); + }; const data = await res.json(); setAccessToken(data.access_token); return data; diff --git a/frontend/src/api/RefreshAPI/Refresh.ts b/frontend/src/api/RefreshAPI/Refresh.ts new file mode 100644 index 0000000..a5f43e7 --- /dev/null +++ b/frontend/src/api/RefreshAPI/Refresh.ts @@ -0,0 +1,13 @@ +import { API_URL, setAccessToken } from "../token"; + +export async function refreshToken(): Promise { + const response = await fetch(`${API_URL}/protected/refresh`, { + method: "POST", + credentials: "include", + }); + if (!response.ok) { + throw new Error("Session update wasn't successful"); + }; + const data = await response.json(); + setAccessToken(data.access_token); +} \ No newline at end of file diff --git a/frontend/src/api/ResetAPI/Reset.ts b/frontend/src/api/ResetAPI/Reset.ts index 4912fae..887fca2 100644 --- a/frontend/src/api/ResetAPI/Reset.ts +++ b/frontend/src/api/ResetAPI/Reset.ts @@ -5,7 +5,9 @@ export async function requestPasswordReset(email: string): Promise { credentials: "include", }); if (!res.ok) { - const body = await res.json().catch(() => null); + const body = await res.json().catch(function(){ + return null + }); throw new Error(typeof body?.detail === "string" ? body.detail : "Не удалось отправить запрос"); } } \ No newline at end of file diff --git a/frontend/src/api/UserAPI/User.ts b/frontend/src/api/UserAPI/User.ts new file mode 100644 index 0000000..f622ccb --- /dev/null +++ b/frontend/src/api/UserAPI/User.ts @@ -0,0 +1,13 @@ +import type { UserOut } from "../../types/UserTypes"; +import { authFetch } from "../token"; + + +export async function fetchUser(): Promise { + const response = await authFetch("/protected/me"); + if (!response.ok) { + throw new Error("Not authenticated"); + } + const data = await response.json() + console.log(data) + return data; +} \ No newline at end of file diff --git a/frontend/src/api/token.ts b/frontend/src/api/token.ts index 14bcfce..0d691bd 100644 --- a/frontend/src/api/token.ts +++ b/frontend/src/api/token.ts @@ -1,17 +1,49 @@ +import { refreshToken } from "./RefreshAPI/Refresh"; + export const API_URL = import.meta.env.VITE_BACKEND_URL; let accessToken: string | null = null; +let refreshTimer: ReturnType | null =null; -export function setAccessToken(token: string | null) { +export function setAccessToken(token: string | null):void{ accessToken = token; } -export function getAccessToken() { +export function getAccessToken():string|null { return accessToken; } export async function authFetch(path: string, init: RequestInit = {}) { const headers = new Headers(init.headers); - if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`); + if (accessToken) { + headers.set("Authorization", `Bearer ${accessToken}`); + }; return fetch(`${API_URL}${path}`, { ...init, headers, credentials: "include" }); } + +export function startRefreshTimer(intervalms:number) { + stopRefreshTimer(); + refreshTimer = setInterval(function () { + refreshToken().catch(function() { + + }); + }, intervalms); +} + +export function stopRefreshTimer() { + if (refreshTimer) clearInterval(refreshTimer); + refreshTimer = null; +} + +export async function logout() { + try { + await authFetch(`/protected/logout`); + } catch (err) { + throw new Error("Something went wrong", { cause: err }); + } + + setAccessToken(null); + stopRefreshTimer(); + localStorage.removeItem("rememberMe"); + sessionStorage.removeItem("rememberMe"); +} diff --git a/frontend/src/features/auth/components/AuthComponent.tsx b/frontend/src/features/auth/components/AuthComponent.tsx index f36d79b..5248bae 100644 --- a/frontend/src/features/auth/components/AuthComponent.tsx +++ b/frontend/src/features/auth/components/AuthComponent.tsx @@ -12,29 +12,30 @@ export function Auth():ReactElement { const [errors, setErrors] = useState<{ email?: string; password?: string }>({}); const navigate = useNavigate(); - const validateEmail = (value: string) => { + function validateEmail(value: string) { if (!value) return "Введите email"; if (!/^\S+@\S+\.\S+$/.test(value)) return "Некорректный email"; return undefined; - }; - const validatePassword = (value: string) => - value ? undefined : "Введите пароль"; + } + function validatePassword(value: string) { + return value ? undefined : "Введите пароль"; + } - const handleSubmit = async (e: SubmitEvent) => { - e.preventDefault(); - const next = { email: validateEmail(email), password: validatePassword(password) }; - setErrors(next); - if (next.email || next.password) return; + async function handleSubmit(e: SubmitEvent) { + e.preventDefault(); + const next = { email: validateEmail(email), password: validatePassword(password) }; + setErrors(next); + if (next.email || next.password) return; - try { - await login(email, password); - persistRememberMe(rememberMe); - navigate("/dashboard"); - } catch (err) { - console.error(err); - setErrors({ password: "Неверный email или пароль" }); + try { + await login(email, password); + persistRememberMe(rememberMe); + navigate("/dashboard"); + } catch (err) { + console.error(err); + setErrors({ password: "Неверный email или пароль" }); + } } - }; return ( <> diff --git a/frontend/src/features/auth/components/ResetComponent.tsx b/frontend/src/features/auth/components/ResetComponent.tsx index abf25cd..7e7a438 100644 --- a/frontend/src/features/auth/components/ResetComponent.tsx +++ b/frontend/src/features/auth/components/ResetComponent.tsx @@ -8,12 +8,12 @@ export function Reset():ReactElement { const [email, setEmail] = useState(""); const [errors, setErrors] = useState<{ email?: string;}>({}); - const validateEmail = (value: string) => { + function validateEmail(value: string) { if (!value) return "Введите email"; if (!/^\S+@\S+\.\S+$/.test(value)) return "Некорректный email"; return undefined; }; - const handleSubmit = async (e: SubmitEvent) => { + async function handleSubmit(e: SubmitEvent) { e.preventDefault(); const next = { email: validateEmail(email), diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index ade9d64..f45cf7e 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,7 +3,17 @@ import { createRoot } from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' import './index.css' import App from './App.tsx' +import { refreshToken } from './api/RefreshAPI/Refresh.ts' +import { getRememberMe } from './utils/rememberMe.ts' +import { startRefreshTimer } from './api/token.ts' + + +if (getRememberMe()) { + await refreshToken().catch(() => {}); + console.log("Timer started"); + startRefreshTimer(10*60*1000); +} createRoot(document.getElementById('root')!).render( diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index ae80c62..5d10a4c 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,9 +1,50 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { fetchUser } from "../api/UserAPI/User"; +import { logout } from "../api/token"; export function DashboardPage() { + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [permissions, setPermissions] = useState>(new Set()); + + useEffect(function () { + async function load() { + try { + const user = await fetchUser(); + + const permissions = new Set(); + + for (const p of user.direct_permissions) { + permissions.add(p.permission); + } + for (const g of user.group) { + for (const p of g.permissions) { + permissions.add(p.permission); + } + } + setPermissions(permissions); + setLoading(false); + } catch { + navigate("/"); + } + } + load(); + }, [navigate]); + if (loading) { + return null; + } + async function handleLogout() { + await logout(); + navigate("/"); + } + return ( <> -

test

+ {permissions.has("admin") && (

test

+ )} + ); } \ No newline at end of file diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 1171ed6..88f9d6e 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -1,7 +1,36 @@ +import { useNavigate } from "react-router-dom"; import { Auth } from "../features/auth/components/AuthComponent"; import { AuthLayout } from "../layouts/AuthLayout"; +import { useEffect, useState } from "react"; +import { fetchUser } from "../api/UserAPI/User"; +import { getAccessToken } from "../api/token"; export function LoginPage(){ + const navigate = useNavigate(); + const [checking, setChecking] = useState(function () { + return !!getAccessToken(); + }); + + + useEffect(function () { + if (!getAccessToken()) return; + + async function check() { + try { + await fetchUser(); + navigate("/dashboard"); + } catch { + setChecking(false); + } + } + check(); + }, [navigate]); + + + + if (checking) { + return null; + } return( diff --git a/frontend/src/utils/rememberMe.ts b/frontend/src/utils/rememberMe.ts index 839b2d5..403d4af 100644 --- a/frontend/src/utils/rememberMe.ts +++ b/frontend/src/utils/rememberMe.ts @@ -3,7 +3,19 @@ export function persistRememberMe(value:boolean):void { if (value) { localStorage.setItem("rememberMe", "True"); + sessionStorage.removeItem("rememberMe"); } else{ + sessionStorage.setItem("rememberMe", "True"); localStorage.removeItem("rememberMe"); } -}; \ No newline at end of file +}; + +export function getRememberMe():boolean { + if (localStorage.getItem("rememberMe") || sessionStorage.getItem("rememberMe")) { + return true; + } + else { + return false; + } +} + From 64234f863998ead716653633a9fa56edc24aaead Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Fri, 2 Oct 2026 17:25:12 +0300 Subject: [PATCH 06/10] fix odd if statement --- frontend/src/api/token.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/token.ts b/frontend/src/api/token.ts index 0d691bd..d686530 100644 --- a/frontend/src/api/token.ts +++ b/frontend/src/api/token.ts @@ -31,7 +31,9 @@ export function startRefreshTimer(intervalms:number) { } export function stopRefreshTimer() { - if (refreshTimer) clearInterval(refreshTimer); + if (refreshTimer) { + clearInterval(refreshTimer); + } refreshTimer = null; } From c2fd16e2d8146494b56abb41b3906811dcc07ab1 Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Sun, 4 Oct 2026 18:50:34 +0300 Subject: [PATCH 07/10] dashboard --- .../dashboard/components/Grid.module.css | 6 ++ .../features/dashboard/components/Grid.tsx | 13 +++ .../dashboard/components/Header.module.css | 7 ++ .../features/dashboard/components/Header.tsx | 13 +++ .../components/Right_sidebar.module.css | 6 ++ .../dashboard/components/Right_sidebar.tsx | 13 +++ .../dashboard/components/Sidebar.module.css | 14 ++++ .../features/dashboard/components/Sidebar.tsx | 19 +++++ frontend/src/index.css | 2 - .../AuthLayout.module.css} | 5 +- .../src/layouts/{ => Auth}/AuthLayout.tsx | 82 +++++++++---------- .../src/layouts/Main/Dashboard.module.css | 36 ++++++++ frontend/src/layouts/Main/Dashboard.tsx | 68 +++++++++++++++ frontend/src/pages/Dashboard.tsx | 58 +++---------- frontend/src/pages/ForgotPasswordPage.tsx | 2 +- frontend/src/pages/LoginPage.tsx | 2 +- 16 files changed, 252 insertions(+), 94 deletions(-) create mode 100644 frontend/src/features/dashboard/components/Grid.module.css create mode 100644 frontend/src/features/dashboard/components/Grid.tsx create mode 100644 frontend/src/features/dashboard/components/Header.module.css create mode 100644 frontend/src/features/dashboard/components/Header.tsx create mode 100644 frontend/src/features/dashboard/components/Right_sidebar.module.css create mode 100644 frontend/src/features/dashboard/components/Right_sidebar.tsx create mode 100644 frontend/src/features/dashboard/components/Sidebar.module.css create mode 100644 frontend/src/features/dashboard/components/Sidebar.tsx rename frontend/src/layouts/{AuthLayout.css => Auth/AuthLayout.module.css} (96%) rename frontend/src/layouts/{ => Auth}/AuthLayout.tsx (61%) create mode 100644 frontend/src/layouts/Main/Dashboard.module.css create mode 100644 frontend/src/layouts/Main/Dashboard.tsx diff --git a/frontend/src/features/dashboard/components/Grid.module.css b/frontend/src/features/dashboard/components/Grid.module.css new file mode 100644 index 0000000..f6c2d68 --- /dev/null +++ b/frontend/src/features/dashboard/components/Grid.module.css @@ -0,0 +1,6 @@ +.grid { + display: grid; + justify-content: center; + align-items: center; + width: 100%; +} \ No newline at end of file diff --git a/frontend/src/features/dashboard/components/Grid.tsx b/frontend/src/features/dashboard/components/Grid.tsx new file mode 100644 index 0000000..a513d2d --- /dev/null +++ b/frontend/src/features/dashboard/components/Grid.tsx @@ -0,0 +1,13 @@ +import type { ReactElement } from "react"; +import styles from "./Grid.module.css" + +export function Grid():ReactElement { + + return ( + <> +
+

Content is here

+
+ + ); +} \ No newline at end of file diff --git a/frontend/src/features/dashboard/components/Header.module.css b/frontend/src/features/dashboard/components/Header.module.css new file mode 100644 index 0000000..c5b373c --- /dev/null +++ b/frontend/src/features/dashboard/components/Header.module.css @@ -0,0 +1,7 @@ + +.profile { + display: grid; + justify-content: end; + align-items: center; + width: 100%; +} \ No newline at end of file diff --git a/frontend/src/features/dashboard/components/Header.tsx b/frontend/src/features/dashboard/components/Header.tsx new file mode 100644 index 0000000..7cf12a9 --- /dev/null +++ b/frontend/src/features/dashboard/components/Header.tsx @@ -0,0 +1,13 @@ +import type { ReactElement } from "react"; +import styles from "./Header.module.css" + +export function Header():ReactElement { + + return ( + <> +
+

profile

+
+ + ); +} \ No newline at end of file diff --git a/frontend/src/features/dashboard/components/Right_sidebar.module.css b/frontend/src/features/dashboard/components/Right_sidebar.module.css new file mode 100644 index 0000000..fcf6dff --- /dev/null +++ b/frontend/src/features/dashboard/components/Right_sidebar.module.css @@ -0,0 +1,6 @@ +.right_sidebar { + display: grid; + align-content: center; + justify-content: center; + height: 100vh; +} \ No newline at end of file diff --git a/frontend/src/features/dashboard/components/Right_sidebar.tsx b/frontend/src/features/dashboard/components/Right_sidebar.tsx new file mode 100644 index 0000000..a24fccc --- /dev/null +++ b/frontend/src/features/dashboard/components/Right_sidebar.tsx @@ -0,0 +1,13 @@ +import type { ReactElement } from "react"; +import styles from "./Right_sidebar.module.css" + +export function Right_sidebar():ReactElement { + + return ( + <> +
+

right_sidebar

+
+ + ); +} \ No newline at end of file diff --git a/frontend/src/features/dashboard/components/Sidebar.module.css b/frontend/src/features/dashboard/components/Sidebar.module.css new file mode 100644 index 0000000..6495a21 --- /dev/null +++ b/frontend/src/features/dashboard/components/Sidebar.module.css @@ -0,0 +1,14 @@ +.sidebar_elements{ + display: grid; + align-content: start; + gap: 15px; +} +.logo { + display: grid; + grid-template-columns: 1fr 1fr; +} +.p { + display: grid; + text-align: center; + place-items: center; +} \ No newline at end of file diff --git a/frontend/src/features/dashboard/components/Sidebar.tsx b/frontend/src/features/dashboard/components/Sidebar.tsx new file mode 100644 index 0000000..5603914 --- /dev/null +++ b/frontend/src/features/dashboard/components/Sidebar.tsx @@ -0,0 +1,19 @@ +import type { ReactElement } from "react"; +import styles from "./Sidebar.module.css" + +export function Sidebar():ReactElement { + + return ( + <> +
+

logo

+

Project name

+
+
+ + + +
+ + ); +} \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index 6254f3c..4f073b7 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -6,8 +6,6 @@ a:focus,a:active{outline: none;} nav,footer,header,aside{display: block;} html,body{ - height:100%;width:100%; - padding: 30px; font-family: 'Manrope', sans-serif; user-select: none; font-size:100%;line-height:1;-ms-text-size-adjust:100%;-moz-text-size-adjust:100%;-webkit-text-size-adjust:100%;} diff --git a/frontend/src/layouts/AuthLayout.css b/frontend/src/layouts/Auth/AuthLayout.module.css similarity index 96% rename from frontend/src/layouts/AuthLayout.css rename to frontend/src/layouts/Auth/AuthLayout.module.css index 60bdf9e..da35bdc 100644 --- a/frontend/src/layouts/AuthLayout.css +++ b/frontend/src/layouts/Auth/AuthLayout.module.css @@ -1,10 +1,9 @@ .wrapper { + position: absolute; + inset: 30px; display: flex; flex-direction: row; - min-height: auto; - width: 100%; overflow: hidden; - height: 100%; box-shadow: 1px 1px 20px 1px rgba(68, 68, 68, 0.5); border-radius: 40px; border: 1px solid #444; diff --git a/frontend/src/layouts/AuthLayout.tsx b/frontend/src/layouts/Auth/AuthLayout.tsx similarity index 61% rename from frontend/src/layouts/AuthLayout.tsx rename to frontend/src/layouts/Auth/AuthLayout.tsx index 55edc2a..548aa21 100644 --- a/frontend/src/layouts/AuthLayout.tsx +++ b/frontend/src/layouts/Auth/AuthLayout.tsx @@ -1,28 +1,28 @@ import type { ReactNode } from "react"; -import "./AuthLayout.css"; -import logo from "../assets/LOGO.png"; -import img_1 from "../assets/paper.png"; -import img_2 from "../assets/PC.png"; -import img_3 from "../assets/graphics.png"; +import styles from "./AuthLayout.module.css"; +import logo from "../../assets/LOGO.png"; +import img_1 from "../../assets/paper.png"; +import img_2 from "../../assets/PC.png"; +import img_3 from "../../assets/graphics.png"; export function AuthLayout({ children }: { children: ReactNode }) { return ( -
-
-
-
-
- LOGO -
-
+
+
+
+
+
+ LOGO +
+
The DisExcel
-
+
Project
-
+

Избавляем от ручной работы.
Переводим процессы из бумаги в Excel,
@@ -31,69 +31,69 @@ export function AuthLayout({ children }: { children: ReactNode }) {

-
-
-
+
+
+
{/* 1 */} -
- img -
Бумага
-
+
+ img +
Бумага
+
Ручная работа, ошибки, потери
-
+
{/* 2 */} -
- img -
Excel
-
+
+ img +
Excel
+
Структура, формулы, автоматизация
-
+
{/* 3 */} -
- img -
1С / Отчеты
-
+
+ img +
1С / Отчеты
+
Готовые процессы, аналитика, контроль
-
-
+
+
{/* 1 */} -
+
Экономим
время
-
+
{/* 2 */} -
+
Снижаем
ошибки
-
+
{/* 3 */} -
+
Автоматизируем
процессы
-
+
{/* 4 */} -
+
diff --git a/frontend/src/layouts/Main/Dashboard.module.css b/frontend/src/layouts/Main/Dashboard.module.css new file mode 100644 index 0000000..9df92b2 --- /dev/null +++ b/frontend/src/layouts/Main/Dashboard.module.css @@ -0,0 +1,36 @@ + +* { + outline: 1px solid red; +} +.wrapper { + display: grid; + grid-template-columns: 20% 1fr; + height: 100vh; +} +.left_sidebar{ + display: grid; + align-content: start; + gap:10px; +} + +.header { + display: grid; +} + +.content { + display: grid; +} + +.wrapper_1 { + display: grid; + grid-template-rows: 5vh 1fr; + height: 100%; +} +.wrapper_2 { + display: grid; + grid-template-columns: 70% 1fr; + height: 100%; +} +.right_sidebar{ + display: grid; +} \ No newline at end of file diff --git a/frontend/src/layouts/Main/Dashboard.tsx b/frontend/src/layouts/Main/Dashboard.tsx new file mode 100644 index 0000000..06275c3 --- /dev/null +++ b/frontend/src/layouts/Main/Dashboard.tsx @@ -0,0 +1,68 @@ +import type { ReactNode } from "react"; +import styles from "./Dashboard.module.css" +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { fetchUser } from "../../api/UserAPI/User"; +import { logout } from "../../api/token"; + + +export function MainLayout({sidebar, header, content, right_sidebar }: { sidebar: ReactNode;header: ReactNode; content: ReactNode; right_sidebar: ReactNode }) { + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [permissions, setPermissions] = useState>(new Set()); + + useEffect(function () { + async function load() { + try { + const user = await fetchUser(); + + const permissions = new Set(); + + for (const p of user.direct_permissions) { + permissions.add(p.permission); + } + for (const g of user.group) { + for (const p of g.permissions) { + permissions.add(p.permission); + } + } + setPermissions(permissions); + setLoading(false); + } catch { + navigate("/"); + } + } + load(); + }, [navigate]); + if (loading) { + return null; + } + async function handleLogout() { + await logout(); + navigate("/"); + } + + return ( + +
+
+ {sidebar} +
+
+
+ {header} + {/* {permissions.has("admin") &&

test

} */} + +
+
+
+ {content} +
+
+ {right_sidebar} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 5d10a4c..aaeccde 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,50 +1,16 @@ -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { fetchUser } from "../api/UserAPI/User"; -import { logout } from "../api/token"; - - +import { MainLayout } from "../layouts/Main/Dashboard"; +import { Sidebar} from "../features/dashboard/components/Sidebar"; +import { Header } from "../features/dashboard/components/Header"; +import { Grid } from "../features/dashboard/components/Grid"; +import { Right_sidebar } from "../features/dashboard/components/Right_sidebar"; export function DashboardPage() { - const navigate = useNavigate(); - const [loading, setLoading] = useState(true); - const [permissions, setPermissions] = useState>(new Set()); - - useEffect(function () { - async function load() { - try { - const user = await fetchUser(); - - const permissions = new Set(); - - for (const p of user.direct_permissions) { - permissions.add(p.permission); - } - for (const g of user.group) { - for (const p of g.permissions) { - permissions.add(p.permission); - } - } - setPermissions(permissions); - setLoading(false); - } catch { - navigate("/"); - } - } - load(); - }, [navigate]); - if (loading) { - return null; - } - async function handleLogout() { - await logout(); - navigate("/"); - } - return ( - <> - {permissions.has("admin") && (

test

- )} - - + } + content={} + sidebar={} + right_sidebar={} + > + ); } \ No newline at end of file diff --git a/frontend/src/pages/ForgotPasswordPage.tsx b/frontend/src/pages/ForgotPasswordPage.tsx index 4a657d1..2008363 100644 --- a/frontend/src/pages/ForgotPasswordPage.tsx +++ b/frontend/src/pages/ForgotPasswordPage.tsx @@ -1,5 +1,5 @@ import { Reset } from "../features/auth/components/ResetComponent"; -import { AuthLayout } from "../layouts/AuthLayout"; +import { AuthLayout } from "../layouts/Auth/AuthLayout"; export function ForgotPasswordPage() { return ( diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 88f9d6e..8a521ff 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -1,6 +1,6 @@ import { useNavigate } from "react-router-dom"; import { Auth } from "../features/auth/components/AuthComponent"; -import { AuthLayout } from "../layouts/AuthLayout"; +import { AuthLayout } from "../layouts/Auth/AuthLayout"; import { useEffect, useState } from "react"; import { fetchUser } from "../api/UserAPI/User"; import { getAccessToken } from "../api/token"; From 7ddc7157b5ba5964f02e56e05ebaa4744871ca07 Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Mon, 5 Oct 2026 09:41:31 +0300 Subject: [PATCH 08/10] permission context --- frontend/src/App.tsx | 6 +++- frontend/src/contexts/PermissionContext.ts | 8 +++++ frontend/src/contexts/PermissionProvider.tsx | 31 ++++++++++++++++++ frontend/src/hooks/usePermissions.ts | 8 +++++ .../src/layouts/Main/Dashboard.module.css | 4 +-- frontend/src/layouts/Main/Dashboard.tsx | 32 +++---------------- frontend/src/utils/permissions.ts | 16 ++++++++++ 7 files changed, 74 insertions(+), 31 deletions(-) create mode 100644 frontend/src/contexts/PermissionContext.ts create mode 100644 frontend/src/contexts/PermissionProvider.tsx create mode 100644 frontend/src/hooks/usePermissions.ts create mode 100644 frontend/src/utils/permissions.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f1abb1b..be8e198 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,13 +3,17 @@ import { Route, Routes } from "react-router-dom"; import { LoginPage } from "./pages/LoginPage"; import { ForgotPasswordPage } from "./pages/ForgotPasswordPage"; import { DashboardPage } from './pages/Dashboard'; +import { PermissionProvider } from './contexts/PermissionProvider'; function App() { return ( } /> } /> - } /> + + + } /> ); } diff --git a/frontend/src/contexts/PermissionContext.ts b/frontend/src/contexts/PermissionContext.ts new file mode 100644 index 0000000..765e94c --- /dev/null +++ b/frontend/src/contexts/PermissionContext.ts @@ -0,0 +1,8 @@ +import { createContext } from "react"; + +export type PermissionsState = { + permissions: Set; + loading: boolean; +}; + +export const PermissionContext = createContext(null); diff --git a/frontend/src/contexts/PermissionProvider.tsx b/frontend/src/contexts/PermissionProvider.tsx new file mode 100644 index 0000000..6506871 --- /dev/null +++ b/frontend/src/contexts/PermissionProvider.tsx @@ -0,0 +1,31 @@ +import { useEffect, useState, type ReactNode } from "react"; +import { useNavigate } from "react-router-dom"; +import { loadPermissions } from "../utils/permissions"; +import { PermissionContext } from "./PermissionContext"; + +export function PermissionProvider({ children }: { children: ReactNode }) { + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [permissions, setPermissions] = useState>(new Set()); + + useEffect(function () { + async function load() { + try { + const response = await loadPermissions(); + setPermissions(response); + setLoading(false); + } catch { + navigate("/"); + } + } + load(); + }, [navigate]); + + return ( + + {children} + + ); +} + + diff --git a/frontend/src/hooks/usePermissions.ts b/frontend/src/hooks/usePermissions.ts new file mode 100644 index 0000000..6bc9c3a --- /dev/null +++ b/frontend/src/hooks/usePermissions.ts @@ -0,0 +1,8 @@ +import { useContext } from "react"; +import { PermissionContext } from "../contexts/PermissionContext"; + +export function usePermissions() { + const ctx = useContext(PermissionContext); + if (!ctx) throw new Error("usePermissions must be used inside PermissionProvider"); + return ctx; +} \ No newline at end of file diff --git a/frontend/src/layouts/Main/Dashboard.module.css b/frontend/src/layouts/Main/Dashboard.module.css index 9df92b2..5b75642 100644 --- a/frontend/src/layouts/Main/Dashboard.module.css +++ b/frontend/src/layouts/Main/Dashboard.module.css @@ -1,7 +1,7 @@ -* { +/* * { outline: 1px solid red; -} +} */ .wrapper { display: grid; grid-template-columns: 20% 1fr; diff --git a/frontend/src/layouts/Main/Dashboard.tsx b/frontend/src/layouts/Main/Dashboard.tsx index 06275c3..47c9652 100644 --- a/frontend/src/layouts/Main/Dashboard.tsx +++ b/frontend/src/layouts/Main/Dashboard.tsx @@ -1,42 +1,18 @@ import type { ReactNode } from "react"; import styles from "./Dashboard.module.css" -import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { fetchUser } from "../../api/UserAPI/User"; import { logout } from "../../api/token"; +import { usePermissions } from "../../hooks/usePermissions"; export function MainLayout({sidebar, header, content, right_sidebar }: { sidebar: ReactNode;header: ReactNode; content: ReactNode; right_sidebar: ReactNode }) { const navigate = useNavigate(); - const [loading, setLoading] = useState(true); - const [permissions, setPermissions] = useState>(new Set()); - - useEffect(function () { - async function load() { - try { - const user = await fetchUser(); - - const permissions = new Set(); - - for (const p of user.direct_permissions) { - permissions.add(p.permission); - } - for (const g of user.group) { - for (const p of g.permissions) { - permissions.add(p.permission); - } - } - setPermissions(permissions); - setLoading(false); - } catch { - navigate("/"); - } - } - load(); - }, [navigate]); + const { permissions, loading } = usePermissions(); + if (loading) { return null; } + async function handleLogout() { await logout(); navigate("/"); diff --git a/frontend/src/utils/permissions.ts b/frontend/src/utils/permissions.ts new file mode 100644 index 0000000..47cc291 --- /dev/null +++ b/frontend/src/utils/permissions.ts @@ -0,0 +1,16 @@ +import { fetchUser } from "../api/UserAPI/User"; + +export async function loadPermissions(): Promise> { + const user = await fetchUser(); + + const permissions = new Set(); + for (const p of user.direct_permissions) { + permissions.add(p.permission); + } + for (const g of user.group) { + for (const p of g.permissions) { + permissions.add(p.permission); + } + } + return permissions; +} \ No newline at end of file From 4a7ea33185a84cd76c2ca310b9f69f40b9e73acb Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Mon, 5 Oct 2026 09:41:59 +0300 Subject: [PATCH 09/10] makefile add frontend --- makefile | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/makefile b/makefile index 239de41..bd02a2f 100644 --- a/makefile +++ b/makefile @@ -1,4 +1,5 @@ -VENV:=source .venv/bin/activate; +VENV:=source .venv/bin/activate && +NPM:=. $${NVM_DIR:-$$HOME/.nvm}/nvm.sh && nvm use 24 && ALLURE:=.venv/allure-2.44.0/bin/allure #linux&macos #ALLURE=.venv\allure-2.44.0\bin\allure #Windows @@ -17,7 +18,7 @@ help: ## .PHONY: run run: ## Run dev local application - docker compose -f docker/compose-dev.yaml --env-file configs/.env --profile local up -d && ${VENV} uvicorn main:app --host 0.0.0.0 --port 8000 --reload + docker compose -f docker/compose-dev.yaml --env-file configs/.env --profile local up -d && $(MAKE) -j2 backend frontend .PHONY: down down: ## Down dev local db @@ -39,6 +40,14 @@ down-dev: ## Down dev application down-prod: ## Down prod application docker compose -f docker/compose-dev.yaml --env-file configs/.env --profile prod down +.PHONY: backend +backend: ## Run backend only + ${VENV} uvicorn main:app --host 0.0.0.0 --port 8000 --reload + +.PHONY: frontend +frontend: ## Run frontend only + ${NPM} yarn --cwd ./frontend dev + ## ## Migration section ## From b80fe40852b9505ced26bfb7b6e7b2b198c7a9cd Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Tue, 6 Oct 2026 11:21:09 +0300 Subject: [PATCH 10/10] dev, prod, local environment fixes --- .dockerignore | 7 ++- configs/.env.example | 6 ++- docker/compose-dev.yaml | 62 ++++++++++++++++++++++++- docker/dockerfile | 54 +++++++++++++++++++++ docker/nginx/nginx.conf | 15 ++++++ frontend/configs/.env.example | 3 +- frontend/src/api/token.ts | 8 +++- frontend/src/layouts/Main/Dashboard.tsx | 3 +- main.py | 2 +- makefile | 3 +- node_modules/.yarn-integrity | 10 ++++ poetry.lock | 62 ++++++++++++------------- src/models/configs_read/env.py | 5 +- yarn.lock | 4 ++ 14 files changed, 202 insertions(+), 42 deletions(-) create mode 100644 docker/nginx/nginx.conf create mode 100644 node_modules/.yarn-integrity create mode 100644 yarn.lock diff --git a/.dockerignore b/.dockerignore index ccca191..b95d181 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,4 +15,9 @@ __pycache__/ # OS мусор .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db + +# frontend +frontend/node_modules +frontend/dist +frontend/configs/.env \ No newline at end of file diff --git a/configs/.env.example b/configs/.env.example index 3e75494..1a44d8a 100644 --- a/configs/.env.example +++ b/configs/.env.example @@ -25,5 +25,9 @@ SMTP_SERVER="change_me" EMAIL_LOGIN="change_me" EMAIL_PASSWORD="change_me" -FRONT_URL="change_me" +FRONT_HOST="change_me" +FRONT_PORT=change_me #int + +BACKEND_PORT=change_me #int + PROD_MODE=bool \ No newline at end of file diff --git a/docker/compose-dev.yaml b/docker/compose-dev.yaml index 53175c7..48f15c4 100644 --- a/docker/compose-dev.yaml +++ b/docker/compose-dev.yaml @@ -16,6 +16,8 @@ services: - DB_HOST=psql #rewrite DB_HOST var to communicate inside the docker network - RABBITMQ_HOST=rabbitmq - REDIS_HOST=redis + - FRONT_HOST=${FRONT_HOST} + - FRONT_PORT=${FRONT_PORT} volumes: - type: bind source: ../src @@ -48,7 +50,7 @@ services: rabbitmq: condition: service_healthy ports: - - "80:8000" + - "${BACKEND_PORT}:8000" entrypoint: ["./entrypoint.sh", "--dev"] backend-prod: @@ -65,6 +67,8 @@ services: - DB_HOST=psql - RABBITMQ_HOST=rabbitmq - REDIS_HOST=redis + - FRONT_HOST=${FRONT_HOST} + - FRONT_PORT=${FRONT_PORT} volumes: - type: bind source: ../configs @@ -89,7 +93,7 @@ services: rabbitmq: condition: service_healthy ports: - - "80:8000" + - "${BACKEND_PORT}:8000" daemons: profiles: ["prod", "dev"] @@ -185,6 +189,60 @@ services: networks: - backend + nginx-dev: + image: "${DOCKER_REGISTRY:-local}/excel-frontend-dev:${IMAGE_TAG:-local}" + container_name: frontend-dev + profiles: ["dev"] + build: + dockerfile: ./docker/dockerfile + context: ../ + target: frontend_dev + init: true + ipc: private + volumes: + - type: bind + source: ../frontend/src + target: /home/excel-project/src + - type: bind + source: ../frontend/public + target: /home/excel-project/public + - type: bind + source: ../frontend/index.html + target: /home/excel-project/index.html + - type: bind + source: ../frontend/vite.config.ts + target: /home/excel-project/vite.config.ts + - type: bind + source: ../frontend/configs + target: /home/excel-project/configs + ports: + - "${FRONT_PORT}:5173" + networks: + - backend + restart: always + entrypoint: ["yarn", "dev", "--host", "0.0.0.0"] + + nginx-prod: + image: "${DOCKER_REGISTRY:-local}/excel-frontend-prod:${IMAGE_TAG:-local}" + container_name: frontend-prod + profiles: ["prod"] + build: + dockerfile: ./docker/dockerfile + context: ../ + target: frontend_prod + environment: + - BACKEND_HOST=backend-prod + - BACKEND_PORT=${BACKEND_PORT} + init: true + ipc: private + ports: + - "${FRONT_PORT}:80" + networks: + - backend + restart: always + + + networks: backend: name: "${BACKEND_NETWORK:-backend_network}" \ No newline at end of file diff --git a/docker/dockerfile b/docker/dockerfile index d8ec64c..34786da 100644 --- a/docker/dockerfile +++ b/docker/dockerfile @@ -37,6 +37,7 @@ RUN pip install --no-cache-dir poetry \ && poetry install --no-root --no-interaction --only main,web \ && pip uninstall -y poetry poetry-core poetry-plugin-export + # --- Stage 2: Python Backend prod --- FROM python:3.14-slim AS prod @@ -64,6 +65,7 @@ USER appuser ENTRYPOINT ["./entrypoint.sh", "--prod"] + # --- Stage 1: Python daemon builder --- FROM python:3.14-slim AS worker-builder @@ -85,6 +87,7 @@ RUN pip install --no-cache-dir poetry \ && poetry install --no-root --no-interaction --only main,daemon \ && pip uninstall -y poetry poetry-core poetry-plugin-export + # --- Stage 2: Python daemons --- FROM python:3.14-slim AS daemon @@ -109,3 +112,54 @@ RUN groupadd --gid 1000 appuser \ USER appuser ENTRYPOINT ["python","-u", "daemon_run.py", "--all"] + + +# --- Stage 1: Node Frontend builder --- + +FROM node:24-alpine AS frontend-builder + +LABEL org.opencontainers.image.title="The-DisExcel-project-prod" +LABEL org.opencontainers.image.source="https://git.homyk.space/MH.Dmitrii/The_DisExcel_project" + +WORKDIR /home/excel-project + +COPY frontend/package.json frontend/yarn.lock ./ + +RUN yarn install --frozen-lockfile + +COPY frontend/ ./ + +RUN yarn build + + +# --- Stage 2: Node Frontend prod --- + +FROM nginx:alpine AS frontend_prod + +LABEL org.opencontainers.image.title="The-DisExcel-project-prod" +LABEL org.opencontainers.image.source="https://git.homyk.space/MH.Dmitrii/The_DisExcel_project" + +COPY --from=frontend-builder /home/excel-project/dist /usr/share/nginx/html + +COPY docker/nginx/nginx.conf /etc/nginx/templates/default.conf.template + + +# --- Stage 1: Node Frontend dev --- + +FROM node:24-alpine AS frontend_dev + +LABEL org.opencontainers.image.title="The-DisExcel-project-prod" +LABEL org.opencontainers.image.source="https://git.homyk.space/MH.Dmitrii/The_DisExcel_project" + +WORKDIR /home/excel-project + +COPY frontend/package.json frontend/yarn.lock ./ + +RUN yarn install --frozen-lockfile + + + + + + + diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf new file mode 100644 index 0000000..1a19b37 --- /dev/null +++ b/docker/nginx/nginx.conf @@ -0,0 +1,15 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri /index.html; + } + + location /protected/ { + proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT}; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} diff --git a/frontend/configs/.env.example b/frontend/configs/.env.example index e8d0ef0..5996ad1 100644 --- a/frontend/configs/.env.example +++ b/frontend/configs/.env.example @@ -1 +1,2 @@ -VITE_BACKEND_URL="change_me" \ No newline at end of file +VITE_BACKEND_HOST="change_me" +VITE_BACKEND_PORT="change_me" \ No newline at end of file diff --git a/frontend/src/api/token.ts b/frontend/src/api/token.ts index d686530..f2cc003 100644 --- a/frontend/src/api/token.ts +++ b/frontend/src/api/token.ts @@ -1,6 +1,12 @@ import { refreshToken } from "./RefreshAPI/Refresh"; -export const API_URL = import.meta.env.VITE_BACKEND_URL; + + +const API_HOST = import.meta.env.VITE_BACKEND_HOST ?? ""; +const API_PORT = import.meta.env.VITE_BACKEND_PORT ?? ""; + +export const API_URL = API_HOST ? `${API_HOST}:${API_PORT}` : ""; + let accessToken: string | null = null; let refreshTimer: ReturnType | null =null; diff --git a/frontend/src/layouts/Main/Dashboard.tsx b/frontend/src/layouts/Main/Dashboard.tsx index 47c9652..f3e7228 100644 --- a/frontend/src/layouts/Main/Dashboard.tsx +++ b/frontend/src/layouts/Main/Dashboard.tsx @@ -7,7 +7,8 @@ import { usePermissions } from "../../hooks/usePermissions"; export function MainLayout({sidebar, header, content, right_sidebar }: { sidebar: ReactNode;header: ReactNode; content: ReactNode; right_sidebar: ReactNode }) { const navigate = useNavigate(); - const { permissions, loading } = usePermissions(); + //const { permissions, loading } = usePermissions(); + const { loading } = usePermissions(); if (loading) { return null; diff --git a/main.py b/main.py index c55bb37..fe07037 100644 --- a/main.py +++ b/main.py @@ -39,7 +39,7 @@ app.include_router(router=protected_router) app.include_router(router=protected_user_action_routes) app.add_middleware( CORSMiddleware, - allow_origins=[env_settings.FRONT_URL], + allow_origins=[f"http://{env_settings.FRONT_HOST}:{env_settings.FRONT_PORT}"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/makefile b/makefile index bd02a2f..118658b 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,4 @@ VENV:=source .venv/bin/activate && -NPM:=. $${NVM_DIR:-$$HOME/.nvm}/nvm.sh && nvm use 24 && ALLURE:=.venv/allure-2.44.0/bin/allure #linux&macos #ALLURE=.venv\allure-2.44.0\bin\allure #Windows @@ -46,7 +45,7 @@ backend: ## Run backend only .PHONY: frontend frontend: ## Run frontend only - ${NPM} yarn --cwd ./frontend dev + yarn --cwd ./frontend dev ## ## Migration section diff --git a/node_modules/.yarn-integrity b/node_modules/.yarn-integrity new file mode 100644 index 0000000..ef91546 --- /dev/null +++ b/node_modules/.yarn-integrity @@ -0,0 +1,10 @@ +{ + "systemParams": "darwin-arm64-137", + "modulesFolders": [], + "flags": [], + "linkedModules": [], + "topLevelPatterns": [], + "lockfileEntries": {}, + "files": [], + "artifacts": {} +} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 19c84ae..ac6acbb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -713,7 +713,7 @@ version = "0.9.63" description = "AI coding assistant skill (Claude Code, CodeBuddy, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["claude"] files = [ {file = "graphifyy-0.9.63-py3-none-any.whl", hash = "sha256:e56aff02d01021c3337b192af6e4d1d2d28b70a35bb4912f777264c3858712d3"}, {file = "graphifyy-0.9.63.tar.gz", hash = "sha256:0d2073903f6cc79a67eae27e1cb9284323da6b29555ecef5c2e35b7a4a1d2a5b"}, @@ -1429,7 +1429,7 @@ version = "3.6" description = "Python package for creating and manipulating graphs and networks" optional = false python-versions = ">=3.11" -groups = ["main"] +groups = ["claude"] files = [ {file = "networkx-3.6-py3-none-any.whl", hash = "sha256:cdb395b105806062473d3be36458d8f1459a4e4b98e236a66c3a48996e07684f"}, {file = "networkx-3.6.tar.gz", hash = "sha256:285276002ad1f7f7da0f7b42f004bcba70d381e936559166363707fdad3d72ad"}, @@ -1452,7 +1452,7 @@ version = "2.5.3" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.12" -groups = ["main"] +groups = ["claude"] files = [ {file = "numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9"}, {file = "numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c"}, @@ -2251,7 +2251,7 @@ version = "3.14.6" description = "rapid fuzzy string matching" optional = false python-versions = ">=3.11" -groups = ["main"] +groups = ["claude"] files = [ {file = "rapidfuzz-3.14.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c0dd0d765184366b6e213a8af3b0b3bb39dad27943bbfb193515d4ff96ac82a"}, {file = "rapidfuzz-3.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0c61cade182f130c9903231946bd1074539121721693a918e7b70382ae802bd8"}, @@ -2667,7 +2667,7 @@ version = "0.25.2" description = "Python bindings to the Tree-sitter parsing library" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20"}, {file = "tree_sitter-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72a510931c3c25f134aac2daf4eb4feca99ffe37a35896d7150e50ac3eee06c7"}, @@ -2717,7 +2717,7 @@ version = "0.25.1" description = "Bash grammar for tree-sitter" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_bash-0.25.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:0e6235f59e366d220dde7d830196bed597d01e853e44d8ccd1a82c5dd2500acf"}, {file = "tree_sitter_bash-0.25.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f4a34a6504c7c5b2a9b8c5c4065531dea19ca2c35026e706cf2eeeebe2c92512"}, @@ -2739,7 +2739,7 @@ version = "0.24.2" description = "C grammar for tree-sitter" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_c-0.24.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:4d4579a8b54f0a442f903d88d3304cab77cd5c2031d4015baa4f2f8e15d6dcb7"}, {file = "tree_sitter_c-0.24.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:97bc80a224d48215d4e6e6376bf30d114f4c317b8145ff1b02afe785d4ba7bdd"}, @@ -2761,7 +2761,7 @@ version = "0.23.5" description = "C# grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_c_sharp-0.23.5-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:61e1981cf21b09ee547b9c4c68e64fb4394325f8fc8d5f6d50d41471eba923ea"}, {file = "tree_sitter_c_sharp-0.23.5-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a75994a11f6fed3f5b8c36ad6a00e5dc43205bd912c43af3a2a54fdf649664eb"}, @@ -2783,7 +2783,7 @@ version = "0.23.4" description = "C++ grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_cpp-0.23.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aacb1759f0efd9dbc25bd8ee88184a340483018869f75412d9c3bc32c039a520"}, {file = "tree_sitter_cpp-0.23.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc3c404d9f0cbd87951213a85440afbf4c31e718f8d907fa9ee12bea4b8d276f"}, @@ -2804,7 +2804,7 @@ version = "0.3.5" description = "Elixir grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_elixir-0.3.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:514078a2f68d27da9a1e6b6e9601b8456faba6260ecfa252e898a848c4f8584d"}, {file = "tree_sitter_elixir-0.3.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:015f537731af690cfa238b0fb76a8af4f0d1a2c54a38563f159926d2967ce650"}, @@ -2826,7 +2826,7 @@ version = "0.6.0" description = "Fortran grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_fortran-0.6.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b6495c4c25cf68785ffd30e615b5481219415761ca66dde14a9577d03075714d"}, {file = "tree_sitter_fortran-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a0fe5929fd91d245aba5a3b414399a296fb9924942a549190cee226e5b1ec96c"}, @@ -2848,7 +2848,7 @@ version = "0.25.0" description = "Go grammar for tree-sitter" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_go-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b852993063a3429a443e7bd0aa376dd7dd329d595819fabf56ac4cf9d7257b54"}, {file = "tree_sitter_go-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:503b81a2b4c31e302869a1de3a352ad0912ccab3df9ac9950197b0a9ceeabd8f"}, @@ -2870,7 +2870,7 @@ version = "0.1.2" description = "Groovy grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_groovy-0.1.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27adb7a4077511782dbd94a12f4635dfb52ccb88f734fe1569393e2d28b18bbd"}, {file = "tree_sitter_groovy-0.1.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:db35a5bdceb826382c7f52d33db0b2075217473f698daf77eb8d4e557a161d51"}, @@ -2891,7 +2891,7 @@ version = "0.23.5" description = "Java grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_java-0.23.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:355ce0308672d6f7013ec913dee4a0613666f4cda9044a7824240d17f38209df"}, {file = "tree_sitter_java-0.23.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:24acd59c4720dedad80d548fe4237e43ef2b7a4e94c8549b0ca6e4c4d7bf6e69"}, @@ -2912,7 +2912,7 @@ version = "0.25.0" description = "JavaScript grammar for tree-sitter" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_javascript-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b70f887fb269d6e58c349d683f59fa647140c410cfe2bee44a883b20ec92e3dc"}, {file = "tree_sitter_javascript-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:8264a996b8845cfce06965152a013b5d9cbb7d199bc3503e12b5682e62bb1de1"}, @@ -2934,7 +2934,7 @@ version = "0.24.8" description = "JSON grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_json-0.24.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:59ac06c6db1877d0e2076bce54a5fddcdd2fc38ca778905662e80fa9ffcea2ab"}, {file = "tree_sitter_json-0.24.8-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:62b4c45b561db31436a81a3f037f71ec29049f4fc9bf5269b6ec3ebaaa35a1cd"}, @@ -2955,7 +2955,7 @@ version = "0.23.1" description = "Julia grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_julia-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:4bd4d8e76ab780a2de9af90cefada494cb174991d74993b6a243f28081e9432b"}, {file = "tree_sitter_julia-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8197c8d9b0cb51421aa2832f3fb539504d7b514cbb1fc79130bb1445c0b4a457"}, @@ -2976,7 +2976,7 @@ version = "1.1.0" description = "Kotlin grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_kotlin-1.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6cca5ef06d090e8494ac1d9f0aac71ed32207d412766b5df7da00d94334181a2"}, {file = "tree_sitter_kotlin-1.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:910b41a580dae00d319e555075f3886a41386d1067931b14c7de504eeae3ae2a"}, @@ -2997,7 +2997,7 @@ version = "0.5.0" description = "Lua grammar for tree-sitter" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_lua-0.5.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cc4f2eb734dc9223bf96c0eeffa78a9485db207d00841e27e52c8b036f2164f7"}, {file = "tree_sitter_lua-0.5.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c14714ad395c4166566f3e4dd0cc0979411684cbcd23702e3c631c3e6eae84fd"}, @@ -3019,7 +3019,7 @@ version = "3.0.2" description = "Objective-C grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_objc-3.0.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bd25b3c4ca99263c0898aa7a362a1b8d9bb642692ae9ddd357755586019b1544"}, {file = "tree_sitter_objc-3.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:9fa8b1221d2651a51cf42e1551c0804e9f48707da70f41f3195910c599b5522b"}, @@ -3040,7 +3040,7 @@ version = "0.24.1" description = "PHP grammar for tree-sitter" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_php-0.24.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:d56e2dcf025450f84a2cdbf4b18a09e6cb88b92e9e6858e63de3d4133ab2e43e"}, {file = "tree_sitter_php-0.24.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:29759c67d4c27a68c227ed82c0b7e4699617b1bd23757d50c081f81a12b4f80d"}, @@ -3060,7 +3060,7 @@ version = "0.26.4" description = "A Powershell grammar for tree-sitter" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_powershell-0.26.4-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:0bf8beac7ed4501d1c52456f8ae9728ab2a5a079325548b06b1bc9746655524e"}, {file = "tree_sitter_powershell-0.26.4-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:b5dde429c9de55b75906e240d6db1cf85417e2fc0a56d7b321810c2cd4cf3f98"}, @@ -3082,7 +3082,7 @@ version = "0.25.0" description = "Python grammar for tree-sitter" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_python-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:14a79a47ddef72f987d5a2c122d148a812169d7484ff5c75a3db9609d419f361"}, {file = "tree_sitter_python-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:480c21dbd995b7fe44813e741d71fed10ba695e7caab627fb034e3828469d762"}, @@ -3104,7 +3104,7 @@ version = "0.23.1" description = "Ruby grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_ruby-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:39f391322d2210843f07081182dbf00f8f69cfbfa4687b9575cac6d324bae443"}, {file = "tree_sitter_ruby-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:aa4ee7433bd42fac22e2dad4a3c0f332292ecf482e610316828c711a0bb7f794"}, @@ -3125,7 +3125,7 @@ version = "0.24.2" description = "Rust grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_rust-0.24.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3620cfd12340efa43082d45df76349ff511893a9c361da2f8d6d51e307020a59"}, {file = "tree_sitter_rust-0.24.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:01a46622735498493f29f3e628a90de95c96a07bfbeb88996243eb986b1cee36"}, @@ -3147,7 +3147,7 @@ version = "0.26.2" description = "Scala grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_scala-0.26.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:beab6a0397d4f45cecaed715f224f444652d6bb1c5318d46c641893cd91de98d"}, {file = "tree_sitter_scala-0.26.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:54d2e92824c7908b7e688f218dc1d65c5684c365cd7a3e8028e8860847aeabd4"}, @@ -3168,7 +3168,7 @@ version = "0.7.3" description = "Swift grammar for tree-sitter" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_swift-0.7.3-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2531ec866c22ea52384e2786e07f3b2bb396c6446428a2df02cc74af3f7e6b6a"}, {file = "tree_sitter_swift-0.7.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ee627e027d0868c552beca13dcdfa9944662b126f642464c5038ee3204e68340"}, @@ -3190,7 +3190,7 @@ version = "0.23.2" description = "TypeScript and TSX grammars for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_typescript-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3cd752d70d8e5371fdac6a9a4df9d8924b63b6998d268586f7d374c9fba2a478"}, {file = "tree_sitter_typescript-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c7cc1b0ff5d91bac863b0e38b1578d5505e718156c9db577c8baea2557f66de8"}, @@ -3211,7 +3211,7 @@ version = "1.0.3" description = "Verilog grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_verilog-1.0.3-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ee20fe0e21c93bf1a10e20c13cbca959eb3c9693194afb90b0567758cbf1744e"}, {file = "tree_sitter_verilog-1.0.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5b9d70d86cf6913abc08766b6180e285d72848c7491a3f3f8e7bb8d8c440049d"}, @@ -3232,7 +3232,7 @@ version = "1.1.2" description = "Zig grammar for tree-sitter" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["claude"] files = [ {file = "tree_sitter_zig-1.1.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e7542354a5edba377b5692b2add4f346501306d455e192974b7e76bf1a61a282"}, {file = "tree_sitter_zig-1.1.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:daa2cdd7c1a2d278f2a917c85993adb6e84d37778bfc350ee9e342872e7f8be2"}, @@ -3445,4 +3445,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = ">=3.13,<4.0" -content-hash = "b92e68793c9822e819514bba50be5bf35dd09dcb9d7d9068f1e489502da2bfe8" +content-hash = "fffbbc108bcb642d179b4d838b676bd848ab2f4af8d56314d056c74248fe5d06" diff --git a/src/models/configs_read/env.py b/src/models/configs_read/env.py index ca14a12..2fb8f1f 100644 --- a/src/models/configs_read/env.py +++ b/src/models/configs_read/env.py @@ -31,7 +31,10 @@ class Env(Base): EMAIL_LOGIN:str EMAIL_PASSWORD:str - FRONT_URL:str + FRONT_HOST:str + FRONT_PORT:int + BACKEND_PORT:int + PROD_MODE:bool model_config=SettingsConfigDict(env_file="configs/.env", extra=None) diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..fb57ccd --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +