Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b492791fe1 | ||
|
|
b80fe40852 | ||
|
|
4a7ea33185 | ||
|
|
7ddc7157b5 | ||
|
|
c2fd16e2d8 | ||
|
|
64234f8639 | ||
|
|
e99aafa36a | ||
|
|
af151f254b | ||
|
|
1fbec06c23 | ||
|
|
bcfd4acfad | ||
|
|
6a6a95e863 |
+6
-1
@@ -15,4 +15,9 @@ __pycache__/
|
||||
|
||||
# OS мусор
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# frontend
|
||||
frontend/node_modules
|
||||
frontend/dist
|
||||
frontend/configs/.env
|
||||
@@ -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
@@ -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}"
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
VITE_BACKEND_URL="change_me"
|
||||
VITE_BACKEND_HOST="change_me"
|
||||
VITE_BACKEND_PORT="change_me"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 : "Не удалось отправить запрос");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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%;}
|
||||
|
||||
+5
-3
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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 (
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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=["*"],
|
||||
|
||||
@@ -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
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"systemParams": "darwin-arm64-137",
|
||||
"modulesFolders": [],
|
||||
"flags": [],
|
||||
"linkedModules": [],
|
||||
"topLevelPatterns": [],
|
||||
"lockfileEntries": {},
|
||||
"files": [],
|
||||
"artifacts": {}
|
||||
}
|
||||
Generated
+31
-31
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user