11 Commits
47 changed files with 710 additions and 126 deletions
+6 -1
View File
@@ -15,4 +15,9 @@ __pycache__/
# OS мусор
.DS_Store
Thumbs.db
Thumbs.db
# frontend
frontend/node_modules
frontend/dist
frontend/configs/.env
+5 -1
View File
@@ -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
+60 -2
View File
@@ -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}"
+54
View File
@@ -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
+15
View File
@@ -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;
}
}
+2 -1
View File
@@ -1 +1,2 @@
VITE_BACKEND_URL="change_me"
VITE_BACKEND_HOST="change_me"
VITE_BACKEND_PORT="change_me"
-4
View File
@@ -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;
}
+6
View File
@@ -2,12 +2,18 @@ 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';
import { PermissionProvider } from './contexts/PermissionProvider';
function App() {
return (
<Routes>
<Route path="/" element={<LoginPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<Route path="/dashboard" element={
<PermissionProvider>
<DashboardPage />
</PermissionProvider>} />
</Routes>
);
}
+3 -1
View File
@@ -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;
+13
View File
@@ -0,0 +1,13 @@
import { API_URL, setAccessToken } from "../token";
export async function refreshToken(): Promise<void> {
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);
}
+3 -1
View File
@@ -5,7 +5,9 @@ export async function requestPasswordReset(email: string): Promise<void> {
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 : "Не удалось отправить запрос");
}
}
+13
View File
@@ -0,0 +1,13 @@
import type { UserOut } from "../../types/UserTypes";
import { authFetch } from "../token";
export async function fetchUser(): Promise<UserOut> {
const response = await authFetch("/protected/me");
if (!response.ok) {
throw new Error("Not authenticated");
}
const data = await response.json()
console.log(data)
return data;
}
+44 -4
View File
@@ -1,17 +1,57 @@
export const API_URL = import.meta.env.VITE_BACKEND_URL;
import { refreshToken } from "./RefreshAPI/Refresh";
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<typeof setInterval> | 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");
}
@@ -0,0 +1,8 @@
import { createContext } from "react";
export type PermissionsState = {
permissions: Set<string>;
loading: boolean;
};
export const PermissionContext = createContext<PermissionsState | null>(null);
@@ -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<Set<string>>(new Set());
useEffect(function () {
async function load() {
try {
const response = await loadPermissions();
setPermissions(response);
setLoading(false);
} catch {
navigate("/");
}
}
load();
}, [navigate]);
return (
<PermissionContext value={{ permissions, loading }}>
{children}
</PermissionContext>
);
}
@@ -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,29 +10,32 @@ 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) => {
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<HTMLFormElement>) => {
e.preventDefault();
const next = { email: validateEmail(email), password: validatePassword(password) };
setErrors(next);
if (next.email || next.password) return;
async function handleSubmit(e: SubmitEvent<HTMLFormElement>) {
e.preventDefault();
const next = { email: validateEmail(email), password: validatePassword(password) };
setErrors(next);
if (next.email || next.password) return;
try {
await login(email, password);
window.location.href = "http://localhost:8000/docs";
} 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 (
<>
@@ -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<HTMLFormElement>) => {
async function handleSubmit(e: SubmitEvent<HTMLFormElement>) {
e.preventDefault();
const next = {
email: validateEmail(email),
@@ -0,0 +1,6 @@
.grid {
display: grid;
justify-content: center;
align-items: center;
width: 100%;
}
@@ -0,0 +1,13 @@
import type { ReactElement } from "react";
import styles from "./Grid.module.css"
export function Grid():ReactElement {
return (
<>
<div className={styles.grid}>
<p>Content is here</p>
</div>
</>
);
}
@@ -0,0 +1,7 @@
.profile {
display: grid;
justify-content: end;
align-items: center;
width: 100%;
}
@@ -0,0 +1,13 @@
import type { ReactElement } from "react";
import styles from "./Header.module.css"
export function Header():ReactElement {
return (
<>
<div className={styles.profile}>
<p>profile</p>
</div>
</>
);
}
@@ -0,0 +1,6 @@
.right_sidebar {
display: grid;
align-content: center;
justify-content: center;
height: 100vh;
}
@@ -0,0 +1,13 @@
import type { ReactElement } from "react";
import styles from "./Right_sidebar.module.css"
export function Right_sidebar():ReactElement {
return (
<>
<div className={styles.right_sidebar}>
<p>right_sidebar</p>
</div>
</>
);
}
@@ -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;
}
@@ -0,0 +1,19 @@
import type { ReactElement } from "react";
import styles from "./Sidebar.module.css"
export function Sidebar():ReactElement {
return (
<>
<div className={styles.logo}>
<p className={styles.p}>logo</p>
<p className={styles.p}>Project name</p>
</div>
<div className={styles.sidebar_elements}>
<button>Dashboard</button>
<button>Excel</button>
<button>Papers</button>
</div>
</>
);
}
+8
View File
@@ -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;
}
-2
View File
@@ -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%;}
@@ -1,10 +1,12 @@
.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;
}
.left_side {
width: 60%;
@@ -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 (
<div className="wrapper">
<div className="left_side">
<header className="header">
<div className="container">
<div className="header_top">
<img className="img_logo" src={logo} alt="LOGO" />
<div className="header_text">
<div className="header_title">
<div className={styles.wrapper}>
<div className={styles.left_side}>
<header className={styles.header}>
<div className={styles.container}>
<div className={styles.header_top}>
<img className={styles.img_logo} src={logo} alt="LOGO" />
<div className={styles.header_text}>
<div className={styles.header_title}>
The DisExcel
</div>
<div className="header_heading">
<div className={styles.header_heading}>
Project
</div>
</div>
</div>
<div className="header_bottom">
<div className={styles.header_bottom}>
<p>
Избавляем от ручной работы. <br />
Переводим процессы из бумаги в Excel, <br />
@@ -31,69 +31,69 @@ export function AuthLayout({ children }: { children: ReactNode }) {
</div>
</div>
</header>
<main className="main">
<div className="contain">
<div className="main_items">
<main className={styles.main}>
<div className={styles.contain}>
<div className={styles.main_items}>
{/* 1 */}
<div className="main_item">
<img className="main_item_img" src={img_1} alt="img" />
<div className="main_item_title">Бумага</div>
<div className="main_item_p">
<div className={styles.main_item}>
<img className={styles.main_item_img} src={img_1} alt="img" />
<div className={styles.main_item_title}>Бумага</div>
<div className={styles.main_item_p}>
Ручная работа, ошибки, потери
</div>
</div>
<div className="main_i"></div>
<div className={styles.main_i}></div>
{/* 2 */}
<div className="main_item">
<img className="main_item_img" src={img_2} alt="img" />
<div className="main_item_title">Excel</div>
<div className="main_item_p">
<div className={styles.main_item}>
<img className={styles.main_item_img} src={img_2} alt="img" />
<div className={styles.main_item_title}>Excel</div>
<div className={styles.main_item_p}>
Структура, формулы, автоматизация
</div>
</div>
<div className="main_i"></div>
<div className={styles.main_i}></div>
{/* 3 */}
<div className="main_item">
<img className="main_item_img" src={img_3} alt="img" />
<div className="main_item_title">1С / Отчеты</div>
<div className="main_item_p">
<div className={styles.main_item}>
<img className={styles.main_item_img} src={img_3} alt="img" />
<div className={styles.main_item_title}>1С / Отчеты</div>
<div className={styles.main_item_p}>
Готовые процессы, аналитика, контроль
</div>
</div>
</div>
</div>
</main>
<section className="section">
<div className="section_items">
<section className={styles.section}>
<div className={styles.section_items}>
{/* 1 */}
<div className="section_item">
<div className={styles.section_item}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<circle cx="24" cy="24" r="16" stroke="#087A55" strokeWidth="3"/>
<path d="M24 14v11l7 4" stroke="#087A55" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
Экономим <br /> время
</div>
<div className="section_line"></div>
<div className={styles.section_line}></div>
{/* 2 */}
<div className="section_item">
<div className={styles.section_item}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<path d="M24 6 38 11v11c0 9-5.8 15.2-14 20-8.2-4.8-14-11-14-20V11z" stroke="#087A55" strokeWidth="3" strokeLinejoin="round"/>
<path d="m17 24 5 5 10-11" stroke="#087A55" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
Снижаем <br /> ошибки
</div>
<div className="section_line"></div>
<div className={styles.section_line}></div>
{/* 3 */}
<div className="section_item">
<div className={styles.section_item}>
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path stroke="#087A55" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.5.5 0 0 0 .12-.63l-1.92-3.32a.5.5 0 0 0-.6-.22l-2.39.96a7.03 7.03 0 0 0-1.62-.94l-.36-2.54a.5.5 0 0 0-.5-.43h-3.84a.5.5 0 0 0-.5.43l-.36 2.54c-.59.24-1.13.56-1.62.94l-2.39-.96a.5.5 0 0 0-.6.22L2.66 8.85a.5.5 0 0 0 .12.63l2.03 1.58c-.05.3-.08.63-.08.94s.02.64.07.94l-2.03 1.58a.5.5 0 0 0-.12.63l1.92 3.32a.5.5 0 0 0 .6.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54a.5.5 0 0 0 .5.43h3.84a.5.5 0 0 0 .5-.43l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96a.5.5 0 0 0 .6-.22l1.92-3.32a.5.5 0 0 0-.12-.63l-2.03-1.58Z"/>
<circle cx="12" cy="12" r="3.5" stroke="#087A55" strokeWidth="1.6"/>
</svg>
Автоматизируем <br /> процессы
</div>
<div className="section_line"></div>
<div className={styles.section_line}></div>
{/* 4 */}
<div className="section_item">
<div className={styles.section_item}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<path d="M24 6 38 11v11c0 9-5.8 15.2-14 20-8.2-4.8-14-11-14-20V11z" stroke="#087A55" strokeWidth="3" strokeLinejoin="round"/>
<path d="m17 24 5 5 10-11" stroke="#087A55" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
@@ -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;
}
+45
View File
@@ -0,0 +1,45 @@
import type { ReactNode } from "react";
import styles from "./Dashboard.module.css"
import { useNavigate } from "react-router-dom";
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 { permissions, loading } = usePermissions();
const { loading } = usePermissions();
if (loading) {
return null;
}
async function handleLogout() {
await logout();
navigate("/");
}
return (
<div className={styles.wrapper}>
<div className={styles.left_sidebar}>
{sidebar}
</div>
<div className={styles.wrapper_1}>
<div className={styles.header}>
{header}
{/* {permissions.has("admin") && <div><p>test</p></div>} */}
<button onClick={handleLogout}>logout</button>
</div>
<div className={styles.wrapper_2}>
<div className={styles.content}>
{content}
</div>
<div className={styles.right_sidebar}>
{right_sidebar}
</div>
</div>
</div>
</div>
);
}
+10
View File
@@ -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(
<StrictMode>
<BrowserRouter>
+16
View File
@@ -0,0 +1,16 @@
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() {
return (
<MainLayout
header={<Header />}
content={<Grid />}
sidebar={<Sidebar />}
right_sidebar={<Right_sidebar />}
>
</MainLayout>
);
}
+1 -1
View File
@@ -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 (
+30 -1
View File
@@ -1,7 +1,36 @@
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";
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(
<AuthLayout>
<Auth />
+16
View File
@@ -0,0 +1,16 @@
import { fetchUser } from "../api/UserAPI/User";
export async function loadPermissions(): Promise<Set<string>> {
const user = await fetchUser();
const permissions = new Set<string>();
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;
}
+21
View File
@@ -0,0 +1,21 @@
export function persistRememberMe(value:boolean):void {
if (value) {
localStorage.setItem("rememberMe", "True");
sessionStorage.removeItem("rememberMe");
} else{
sessionStorage.setItem("rememberMe", "True");
localStorage.removeItem("rememberMe");
}
};
export function getRememberMe():boolean {
if (localStorage.getItem("rememberMe") || sessionStorage.getItem("rememberMe")) {
return true;
}
else {
return false;
}
}
+1 -1
View File
@@ -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=["*"],
+10 -2
View File
@@ -1,4 +1,4 @@
VENV:=source .venv/bin/activate;
VENV:=source .venv/bin/activate &&
ALLURE:=.venv/allure-2.44.0/bin/allure #linux&macos
#ALLURE=.venv\allure-2.44.0\bin\allure #Windows
@@ -17,7 +17,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 +39,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
yarn --cwd ./frontend dev
##
## Migration section
##
+10
View File
@@ -0,0 +1,10 @@
{
"systemParams": "darwin-arm64-137",
"modulesFolders": [],
"flags": [],
"linkedModules": [],
"topLevelPatterns": [],
"lockfileEntries": {},
"files": [],
"artifacts": {}
}
Generated
+31 -31
View File
@@ -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"
+9 -6
View File
@@ -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
+4 -1
View File
@@ -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)
+4
View File
@@ -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
+3 -3
View File
@@ -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
+28
View File
@@ -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)
+4
View File
@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1