1
0
mirror of https://gitlab.com/MoonTestUse1/AdministrationItDepartmens.git synced 2025-08-14 00:25:46 +02:00

Testing workable

This commit is contained in:
MoonTestUse1
2025-02-07 00:43:33 +06:00
parent 8543d7fe88
commit 6db95a5eb0
37 changed files with 911 additions and 454 deletions

View File

@@ -8,34 +8,22 @@ stages:
variables:
SECRET_KEY: "your-super-secret-key-123"
test-backend:
test:
image: python:3.11
stage: test
before_script:
- python -V
- python -m pip install --upgrade pip
- pip install pytest pytest-cov
- pip install pytest
- pip install -r backend/requirements.txt
script:
- cd backend
- python -m pytest -v tests/test_health.py
- python -m pytest -v
only:
- main
- Testing
test-frontend:
image: node:18
stage: test
before_script:
- cd frontend
- npm install
script:
- npm run test
only:
- main
- Testing
build-backend:
build_backend:
stage: build
image: docker:latest
variables:
@@ -57,7 +45,7 @@ build-backend:
only:
- main
build-frontend:
build_frontend:
stage: build
image: docker:latest
variables:

View File

@@ -1,4 +1,4 @@
# 22Administration Projectss
# Administration Project
[![pipeline status](https://gitlab.com/mysite7215201/Administration/badges/main/pipeline.svg)](https://gitlab.com/mysite7215201/Administration/-/commits/main)

View File

@@ -1,44 +1,36 @@
"""Configuration module"""
import os
from pydantic_settings import BaseSettings
"""Settings configuration"""
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Application settings"""
PROJECT_NAME: str = "Employee Request System"
PROJECT_NAME: str = "Support Service"
VERSION: str = "1.0.0"
API_V1_STR: str = "/api"
# Database
POSTGRES_USER: str = os.getenv("POSTGRES_USER", "postgres")
POSTGRES_PASSWORD: str = os.getenv("POSTGRES_PASSWORD", "postgres")
POSTGRES_HOST: str = os.getenv("POSTGRES_HOST", "localhost")
POSTGRES_PORT: str = os.getenv("POSTGRES_PORT", "5432")
POSTGRES_DB: str = os.getenv("POSTGRES_DB", "app")
DATABASE_URL: str = "postgresql://postgres:postgres123@db:5432/support_db"
# JWT
SECRET_KEY: str = os.getenv("SECRET_KEY", "your-secret-key-super-secret")
SECRET_KEY: str = "your-secret-key"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# Redis
REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
REDIS_HOST: str = "redis"
REDIS_PORT: int = 6379
# Telegram
TELEGRAM_BOT_TOKEN: str = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID: str = os.getenv("TELEGRAM_CHAT_ID", "")
model_config = {
"case_sensitive": True,
"env_file": ".env",
"extra": "allow"
}
# Admin
ADMIN_USERNAME: str = "admin"
ADMIN_PASSWORD: str = "admin123"
def get_database_url(self) -> str:
"""Get database URL"""
return os.getenv(
"DATABASE_URL",
f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
)
# Telegram
TELEGRAM_BOT_TOKEN: str = "your-bot-token"
TELEGRAM_CHAT_ID: str = "your-chat-id"
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=True
)
settings = Settings()

View File

@@ -1,6 +1,6 @@
"""Employee CRUD operations"""
from sqlalchemy.orm import Session
from typing import List, Optional
from typing import Optional, List
from ..models.employee import Employee
from ..schemas.employee import EmployeeCreate, EmployeeUpdate
from ..utils.loggers import auth_logger
@@ -13,42 +13,58 @@ def get_employee(db: Session, employee_id: int) -> Optional[Employee]:
"""Get employee by ID"""
return db.query(Employee).filter(Employee.id == employee_id).first()
def get_employee_by_credentials(db: Session, first_name: str, last_name: str) -> Optional[Employee]:
"""Get employee by first name and last name"""
return db.query(Employee).filter(
Employee.first_name == first_name,
Employee.last_name == last_name
).first()
def get_employee_by_email(db: Session, email: str) -> Optional[Employee]:
"""Get employee by email"""
return db.query(Employee).filter(Employee.email == email).first()
def create_employee(db: Session, employee: EmployeeCreate, hashed_password: str) -> Employee:
"""Create new employee"""
db_employee = Employee(
first_name=employee.first_name,
last_name=employee.last_name,
department=employee.department,
office=employee.office,
hashed_password=hashed_password,
is_admin=employee.is_admin
)
db.add(db_employee)
db.commit()
db.refresh(db_employee)
return db_employee
def update_employee(db: Session, employee_id: int, employee: EmployeeUpdate) -> Optional[Employee]:
"""Update employee data"""
db_employee = get_employee(db, employee_id)
if db_employee:
for key, value in employee.dict(exclude_unset=True).items():
setattr(db_employee, key, value)
try:
db_employee = Employee(
email=employee.email,
full_name=employee.full_name,
hashed_password=hashed_password,
is_active=employee.is_active,
is_admin=employee.is_admin,
department=employee.department
)
db.add(db_employee)
db.commit()
db.refresh(db_employee)
return db_employee
return db_employee
except Exception as e:
auth_logger.error(f"Error creating employee: {e}")
db.rollback()
raise
def update_employee(db: Session, employee_id: int, employee: EmployeeUpdate) -> Optional[Employee]:
"""Update employee"""
db_employee = get_employee(db, employee_id)
if not db_employee:
return None
for field, value in employee.model_dump(exclude_unset=True).items():
setattr(db_employee, field, value)
try:
db.commit()
db.refresh(db_employee)
return db_employee
except Exception as e:
auth_logger.error(f"Error updating employee: {e}")
db.rollback()
raise
def delete_employee(db: Session, employee_id: int) -> Optional[Employee]:
"""Delete employee"""
db_employee = get_employee(db, employee_id)
if db_employee:
db.delete(db_employee)
db.commit()
return db_employee
try:
db.delete(db_employee)
db.commit()
return db_employee
except Exception as e:
auth_logger.error(f"Error deleting employee: {e}")
db.rollback()
raise
return None

View File

@@ -8,7 +8,13 @@ from . import employees
def create_request(db: Session, request: RequestCreate, employee_id: int) -> Request:
"""Create new request"""
# Получаем данные сотрудника
employee = employees.get_employee(db, employee_id)
if not employee:
raise ValueError("Employee not found")
db_request = Request(
department=employee.department, # Берем отдел из данных сотрудника
request_type=request.request_type,
description=request.description,
priority=request.priority,
@@ -24,6 +30,27 @@ def get_request(db: Session, request_id: int) -> Optional[Request]:
"""Get request by ID"""
return db.query(Request).filter(Request.id == request_id).first()
def get_request_details(db: Session, request_id: int) -> Optional[Dict]:
"""Get detailed request information including employee data"""
request = get_request(db, request_id)
if not request:
return None
employee = employees.get_employee(db, request.employee_id)
if not employee:
return None
return {
"id": request.id,
"request_type": request.request_type,
"description": request.description,
"priority": request.priority,
"status": request.status,
"department": request.department,
"created_at": request.created_at.isoformat(),
"employee_full_name": employee.full_name
}
def get_employee_requests(db: Session, employee_id: int) -> list[Request]:
"""Get employee's requests"""
return db.query(Request).filter(Request.employee_id == employee_id).all()
@@ -53,6 +80,12 @@ def get_statistics(db: Session) -> Dict:
func.count(Request.id)
).group_by(Request.status).all()
)
# Добавляем статусы с нулевым количеством
for status in RequestStatus:
if status not in by_status:
by_status[status] = 0
return {
"total": total,
"by_status": by_status

View File

@@ -3,9 +3,9 @@ from sqlalchemy.orm import Session
from typing import Optional
from ..models.token import Token
def create_token(db: Session, token: str, employee_id: int) -> Token:
def create_token(db: Session, token: str, user_id: int) -> Token:
"""Create new token"""
db_token = Token(token=token, employee_id=employee_id)
db_token = Token(token=token, user_id=user_id)
db.add(db_token)
db.commit()
db.refresh(db_token)
@@ -24,8 +24,8 @@ def delete_token(db: Session, token: str) -> bool:
return True
return False
def delete_employee_tokens(db: Session, employee_id: int) -> bool:
"""Delete all tokens for an employee"""
db.query(Token).filter(Token.employee_id == employee_id).delete()
def delete_user_tokens(db: Session, user_id: int) -> bool:
"""Delete all tokens for a user"""
db.query(Token).filter(Token.user_id == user_id).delete()
db.commit()
return True

View File

@@ -1,34 +1,22 @@
"""Database module"""
"""Database configuration"""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from typing import Generator
from sqlalchemy.orm import sessionmaker
from .core.config import settings
from .db.base import Base
# Create base class for models
Base = declarative_base()
# Для создания таблиц импортируем модели
from .models.employee import Employee # noqa
from .models.request import Request # noqa
from .models.token import Token # noqa
# Create database engine
engine = create_engine(
settings.get_database_url(),
pool_pre_ping=True,
pool_size=5,
max_overflow=10
)
SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL
# Create session factory
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db() -> Generator:
"""Get database session"""
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def init_db() -> None:
"""Initialize database"""
# Import models here to avoid circular imports
from .db.base import Base # noqa: F811
Base.metadata.create_all(bind=engine)
db.close()

View File

@@ -1,8 +1,8 @@
"""Import all models for Alembic autogenerate support"""
from app.db.base_class import Base # noqa
from app.models.employee import Employee # noqa
from app.models.request import Request # noqa
from app.models.token import Token # noqa
from app.db.base_class import Base
from app.models.employee import Employee
from app.models.request import Request
from app.models.token import Token
# Import all models for Alembic autogenerate support
# Импортируем все модели, чтобы Alembic мог их обнаружить
__all__ = ["Base", "Employee", "Request", "Token"]

View File

@@ -1,34 +1,40 @@
"""Main application module"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from .core.config import settings
from .routers import auth, employees, requests, admin
from .db.base import Base
from .database import engine
from . import models
from .routers import admin, employees, requests, auth, statistics
app = FastAPI(
title=settings.PROJECT_NAME,
openapi_url=f"{settings.API_V1_STR}/openapi.json"
# Включаем автоматическое перенаправление со слэшем
redirect_slashes=True,
# Добавляем описание API
title="Support System API",
description="API для системы поддержки",
version="1.0.0"
)
# Настройка CORS
# CORS configuration
origins = [
"http://localhost",
"http://localhost:8080",
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://127.0.0.1:8080",
"http://185.139.70.62", # Добавляем ваш production домен
]
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"]
)
# Health check endpoint
@app.get("/api/health")
async def health_check():
"""Health check endpoint"""
return {"status": "ok"}
# Подключаем роутеры
app.include_router(auth.router, prefix=settings.API_V1_STR, tags=["auth"])
app.include_router(employees.router, prefix=settings.API_V1_STR, tags=["employees"])
app.include_router(requests.router, prefix=settings.API_V1_STR, tags=["requests"])
app.include_router(admin.router, prefix=settings.API_V1_STR, tags=["admin"])
# Include routers
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(employees.router, prefix="/api/employees", tags=["employees"])
app.include_router(requests.router, prefix="/api/requests", tags=["requests"])
app.include_router(admin.router, prefix="/api/admin", tags=["admin"])
app.include_router(statistics.router, prefix="/api/statistics", tags=["statistics"])

View File

@@ -1,6 +1,5 @@
"""Models package"""
"""Models initialization"""
from .employee import Employee
from .request import Request
from .token import Token
__all__ = ["Employee", "Request", "Token"]
__all__ = ['Employee', 'Request']

View File

@@ -1,4 +1,7 @@
"""Base model class"""
"""Base models and imports"""
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Enum
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
Base = declarative_base()

View File

@@ -1,22 +1,20 @@
"""Employee model"""
from sqlalchemy import Column, Integer, String, Boolean
from sqlalchemy import Column, Integer, String, DateTime, Boolean
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from ..db.base_class import Base
from app.db.base_class import Base
class Employee(Base):
"""Employee model"""
__tablename__ = "employees"
id = Column(Integer, primary_key=True, index=True)
first_name = Column(String, nullable=False)
last_name = Column(String, nullable=False)
department = Column(String, nullable=False)
office = Column(String, nullable=False)
email = Column(String, unique=True, index=True, nullable=False)
full_name = Column(String, nullable=False)
hashed_password = Column(String, nullable=False)
is_active = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
department = Column(String, nullable=True)
# Отношения
requests = relationship("Request", back_populates="employee", cascade="all, delete-orphan")
tokens = relationship("Token", back_populates="employee", cascade="all, delete-orphan")
# Определяем отношение к Request
requests = relationship("Request", back_populates="employee", cascade="all, delete-orphan")

View File

@@ -1,41 +1,32 @@
"""Request model"""
from enum import Enum
from sqlalchemy import Column, Integer, String, Enum as SQLEnum, ForeignKey, DateTime
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from datetime import datetime
from ..database import Base
from app.db.base_class import Base
class RequestStatus(str, Enum):
"""Request status enum"""
NEW = "new"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
REJECTED = "rejected"
class RequestPriority(str, Enum):
"""Request priority enum"""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class RequestType(str, Enum):
"""Request type enum"""
VACATION = "vacation"
SICK_LEAVE = "sick_leave"
EQUIPMENT = "equipment"
OTHER = "other"
class Request(Base):
"""Request model"""
__tablename__ = "requests"
id = Column(Integer, primary_key=True, index=True)
request_type = Column(SQLEnum(RequestType), nullable=False)
description = Column(String, nullable=False)
priority = Column(SQLEnum(RequestPriority), nullable=False, default=RequestPriority.MEDIUM)
status = Column(SQLEnum(RequestStatus), nullable=False, default=RequestStatus.NEW)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
employee_id = Column(Integer, ForeignKey("employees.id"), nullable=False)
department = Column(String, index=True)
request_type = Column(String, index=True)
description = Column(String)
priority = Column(String)
status = Column(String, default=RequestStatus.NEW)
employee_id = Column(Integer, ForeignKey("employees.id"))
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Определяем отношение к Employee
employee = relationship("Employee", back_populates="requests")

View File

@@ -1,16 +1,12 @@
"""Token model"""
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from ..db.base_class import Base
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.sql import func
from app.db.base_class import Base
class Token(Base):
"""Token model"""
__tablename__ = "tokens"
id = Column(Integer, primary_key=True, index=True)
token = Column(String, unique=True, index=True, nullable=False)
employee_id = Column(Integer, ForeignKey("employees.id", ondelete="CASCADE"), nullable=False)
# Отношения
employee = relationship("Employee", back_populates="tokens")
token = Column(String, unique=True, index=True)
user_id = Column(Integer, index=True) # ID сотрудника из таблицы employees
created_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@@ -6,7 +6,7 @@ from typing import Optional
from ..database import get_db
from ..crud import employees
from ..schemas.auth import Token, LoginCredentials
from ..schemas.auth import Token
from ..utils.auth import verify_password
from ..utils.jwt import create_and_save_token
@@ -19,18 +19,8 @@ async def login_for_access_token(
db: Session = Depends(get_db)
):
"""Авторизация сотрудника"""
# Разделяем username на имя и фамилию
try:
first_name, last_name = form_data.username.split()
except ValueError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Username should be in format: 'First Last'",
headers={"WWW-Authenticate": "Bearer"},
)
# Проверяем учетные данные сотрудника
employee = employees.get_employee_by_credentials(db, first_name, last_name)
employee = employees.get_employee_by_email(db, form_data.username)
if not employee or not verify_password(form_data.password, employee.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -52,18 +42,8 @@ async def admin_login(
db: Session = Depends(get_db)
):
"""Авторизация администратора"""
# Разделяем username на имя и фамилию
try:
first_name, last_name = form_data.username.split()
except ValueError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Username should be in format: 'First Last'",
headers={"WWW-Authenticate": "Bearer"},
)
# Проверяем учетные данные администратора
employee = employees.get_employee_by_credentials(db, first_name, last_name)
employee = employees.get_employee_by_email(db, form_data.username)
if not employee or not employee.is_admin or not verify_password(form_data.password, employee.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,

View File

@@ -1,10 +1,9 @@
"""Schemas package"""
from .employee import Employee, EmployeeCreate, EmployeeUpdate
from .request import Request, RequestCreate, RequestUpdate
from .auth import Token, TokenData, LoginCredentials
from .auth import Token, TokenData
__all__ = [
'Employee', 'EmployeeCreate', 'EmployeeUpdate',
'Request', 'RequestCreate', 'RequestUpdate',
'Token', 'TokenData', 'LoginCredentials'
'Token', 'TokenData'
]

View File

@@ -1,18 +1,32 @@
"""Authentication schemas"""
from pydantic import BaseModel
from typing import Optional
from pydantic import BaseModel, ConfigDict
class AdminLogin(BaseModel):
username: str
password: str
model_config = ConfigDict(from_attributes=True)
class EmployeeLogin(BaseModel):
last_name: str
password: str
model_config = ConfigDict(from_attributes=True)
class EmployeeResponse(BaseModel):
id: int
first_name: str
last_name: str
department: str
office: str
access_token: str
class Token(BaseModel):
"""Token schema"""
access_token: str
token_type: str
class TokenData(BaseModel):
"""Token data schema"""
employee_id: Optional[int] = None
employee_id: int | None = None
is_admin: bool = False
class LoginCredentials(BaseModel):
"""Login credentials schema"""
username: str # В формате "Имя Фамилия"
password: str
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,33 +1,32 @@
"""Employee schemas"""
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from datetime import datetime
from typing import Optional
class EmployeeBase(BaseModel):
"""Base employee schema"""
first_name: str
last_name: str
email: str
full_name: str
department: str
office: str
is_active: bool = True
is_admin: bool = False
model_config = ConfigDict(from_attributes=True)
class EmployeeCreate(EmployeeBase):
"""Employee creation schema"""
password: str
class EmployeeUpdate(BaseModel):
"""Employee update schema"""
first_name: Optional[str] = None
last_name: Optional[str] = None
email: Optional[str] = None
full_name: Optional[str] = None
department: Optional[str] = None
office: Optional[str] = None
password: Optional[str] = None
is_active: Optional[bool] = None
is_admin: Optional[bool] = None
model_config = ConfigDict(from_attributes=True)
class Employee(EmployeeBase):
"""Employee schema"""
id: int
is_active: bool
created_at: datetime
class Config:
"""Pydantic config"""
from_attributes = True
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,30 +1,29 @@
"""Request schemas"""
from pydantic import BaseModel
from datetime import datetime
from pydantic import BaseModel, ConfigDict
from typing import Optional
from ..models.request import RequestStatus, RequestPriority, RequestType
from datetime import datetime
from ..models.request import RequestStatus, RequestPriority
class RequestBase(BaseModel):
"""Base request schema"""
request_type: RequestType
request_type: str
description: str
priority: RequestPriority = RequestPriority.MEDIUM
priority: RequestPriority
model_config = ConfigDict(from_attributes=True)
class RequestCreate(RequestBase):
"""Request create schema"""
pass
class RequestUpdate(BaseModel):
status: RequestStatus
model_config = ConfigDict(from_attributes=True)
class Request(RequestBase):
"""Request schema"""
id: int
status: RequestStatus
created_at: datetime
employee_id: int
department: Optional[str] = None
created_at: datetime
class Config:
"""Pydantic config"""
from_attributes = True
class RequestUpdate(BaseModel):
"""Request update schema"""
status: RequestStatus
model_config = ConfigDict(from_attributes=True)

View File

@@ -8,6 +8,6 @@ class Token(BaseModel):
model_config = ConfigDict(from_attributes=True)
class TokenData(BaseModel):
employee_id: int | None = None
user_id: int | None = None
model_config = ConfigDict(from_attributes=True)

View File

@@ -3,10 +3,10 @@ from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from passlib.context import CryptContext
from sqlalchemy.orm import Session
import re
from .jwt import verify_token
from ..database import get_db
from ..crud import employees
from ..models.employee import Employee
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@@ -34,20 +34,16 @@ def get_current_admin(
try:
token = credentials.credentials
token_data = verify_token(token, db)
if not token_data:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
payload = verify_token(token, db)
employee_id = int(payload.get("sub"))
# Проверяем, что это админ
employee = employees.get_employee(db, token_data.employee_id)
# Получаем сотрудника из БД
from ..crud.employees import get_employee
employee = get_employee(db, employee_id)
if not employee or not employee.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
@@ -55,7 +51,7 @@ def get_current_admin(
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
@@ -73,20 +69,16 @@ def get_current_employee(
try:
token = credentials.credentials
token_data = verify_token(token, db)
if not token_data:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
payload = verify_token(token, db)
employee_id = int(payload.get("sub"))
# Проверяем существование сотрудника
employee = employees.get_employee(db, token_data.employee_id)
# Получаем сотрудника из БД
from ..crud.employees import get_employee
employee = get_employee(db, employee_id)
if not employee:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Employee not found",
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
@@ -94,6 +86,6 @@ def get_current_employee(
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)

View File

@@ -1,67 +1,82 @@
"""JWT utilities"""
from datetime import datetime, timedelta
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from typing import Optional
from jose import JWTError, jwt
from fastapi import HTTPException, status
from redis import Redis
from sqlalchemy.orm import Session
from ..core.config import settings
from ..core.test_config import test_settings
from ..models.token import Token
from ..schemas.auth import TokenData
from ..crud.employees import get_employee
def get_settings():
"""Get settings based on environment"""
return test_settings if test_settings.TESTING else settings
redis = Redis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
decode_responses=True
)
def create_access_token(data: dict) -> str:
"""Create access token"""
to_encode = data.copy()
config = get_settings()
expire = datetime.utcnow() + timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, config.SECRET_KEY, algorithm=config.ALGORITHM)
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def verify_token(token: str) -> Optional[int]:
"""Verify token and return employee_id"""
def verify_token(token: str, db: Session) -> dict:
try:
config = get_settings()
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[config.ALGORITHM])
employee_id = int(payload.get("sub"))
if employee_id is None:
return None
return employee_id
except (JWTError, ValueError):
return None
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
user_id: int = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
)
# Проверяем токен в Redis
if not redis.get(f"token:{token}"):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
)
return payload
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
)
def verify_token_in_db(token: str, db: Session) -> Optional[TokenData]:
"""Verify token in database"""
employee_id = verify_token(token)
if employee_id is None:
return None
# Проверяем, что токен существует в базе
db_token = db.query(Token).filter(Token.token == token).first()
if not db_token:
return None
return TokenData(employee_id=employee_id)
def create_and_save_token(employee_id: int, db: Session) -> str:
"""Create and save token"""
# Создаем токен
access_token = create_access_token({"sub": str(employee_id)})
def create_and_save_token(user_id: int, db: Session) -> str:
# Создаем JWT токен
access_token = create_access_token({"sub": str(user_id)})
# Удаляем старые токены пользователя
db.query(Token).filter(Token.employee_id == employee_id).delete()
# Сохраняем новый токен в базу
# Сохраняем в БД
db_token = Token(
token=access_token,
employee_id=employee_id
user_id=user_id
)
db.add(db_token)
db.commit()
db.refresh(db_token)
return access_token
# Кэшируем в Redis
redis.setex(
f"token:{access_token}",
timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES),
"valid"
)
return access_token
def get_current_employee(token: str, db: Session):
payload = verify_token(token, db)
employee_id = int(payload.get("sub"))
if employee_id == -1: # Для админа
return {"is_admin": True}
employee = get_employee(db, employee_id)
if employee is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Employee not found",
)
return employee

View File

@@ -3,17 +3,20 @@ from aiogram import Bot
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
import asyncio
from datetime import datetime
import os
from logging import getLogger
from ..models.request import RequestStatus, RequestPriority
from ..crud import requests
from ..database import get_db
from ..core.config import settings
# Initialize logger
logger = getLogger(__name__)
# Initialize bot with token from settings
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
# Initialize bot with token
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "7677506032:AAHduD5EePz3bE23DKlo35KoOp2_9lZuS34")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "5057752127")
bot = Bot(token=TELEGRAM_BOT_TOKEN)
def format_priority(priority: str) -> str:
"""Format priority with emoji"""
@@ -56,7 +59,7 @@ async def send_request_notification(request_id: int):
)
await bot.send_message(
chat_id=settings.TELEGRAM_CHAT_ID,
chat_id=TELEGRAM_CHAT_ID,
text=message,
parse_mode="HTML"
)

View File

@@ -1,79 +1,5 @@
-- Создаем основную базу данных
CREATE DATABASE app;
\c app;
-- Connect to the database
\c support_db;
-- Создаем таблицы для основной базы данных
CREATE TABLE employees (
id SERIAL PRIMARY KEY,
first_name VARCHAR NOT NULL,
last_name VARCHAR NOT NULL,
department VARCHAR NOT NULL,
office VARCHAR NOT NULL,
hashed_password VARCHAR NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
is_admin BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE requests (
id SERIAL PRIMARY KEY,
request_type VARCHAR NOT NULL,
description TEXT NOT NULL,
priority VARCHAR NOT NULL,
status VARCHAR NOT NULL DEFAULT 'new',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
employee_id INTEGER NOT NULL REFERENCES employees(id)
);
CREATE TABLE tokens (
id SERIAL PRIMARY KEY,
token VARCHAR UNIQUE NOT NULL,
employee_id INTEGER NOT NULL REFERENCES employees(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Создаем индексы
CREATE INDEX idx_employees_last_name ON employees(last_name);
CREATE INDEX idx_requests_employee_id ON requests(employee_id);
CREATE INDEX idx_requests_status ON requests(status);
CREATE INDEX idx_tokens_token ON tokens(token);
-- Создаем тестовую базу данных
CREATE DATABASE test_app;
\c test_app;
-- Создаем те же таблицы для тестовой базы данных
CREATE TABLE employees (
id SERIAL PRIMARY KEY,
first_name VARCHAR NOT NULL,
last_name VARCHAR NOT NULL,
department VARCHAR NOT NULL,
office VARCHAR NOT NULL,
hashed_password VARCHAR NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
is_admin BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE requests (
id SERIAL PRIMARY KEY,
request_type VARCHAR NOT NULL,
description TEXT NOT NULL,
priority VARCHAR NOT NULL,
status VARCHAR NOT NULL DEFAULT 'new',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
employee_id INTEGER NOT NULL REFERENCES employees(id)
);
CREATE TABLE tokens (
id SERIAL PRIMARY KEY,
token VARCHAR UNIQUE NOT NULL,
employee_id INTEGER NOT NULL REFERENCES employees(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Создаем индексы в тестовой базе
CREATE INDEX idx_employees_last_name ON employees(last_name);
CREATE INDEX idx_requests_employee_id ON requests(employee_id);
CREATE INDEX idx_requests_status ON requests(status);
CREATE INDEX idx_tokens_token ON tokens(token);
-- Create extensions if needed
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

View File

@@ -1,18 +1,17 @@
fastapi>=0.100.0
uvicorn>=0.22.0
sqlalchemy>=2.0.0
psycopg2-binary>=2.9.6
python-jose[cryptography]>=3.3.0
fastapi==0.110.0
uvicorn==0.27.1
sqlalchemy==2.0.27
pydantic==2.5.2
pydantic-settings==2.2.1
python-multipart==0.0.9
python-jose[cryptography]==3.3.0
passlib[bcrypt]>=1.7.4
python-multipart>=0.0.6
pydantic>=2.0.0
pydantic-settings>=2.0.0
pytest>=7.4.0
pytest-cov>=4.1.0
pytest-timeout>=2.1.0
pytest-xdist>=3.3.1
pytest-mock>=3.10.0
httpx>=0.24.1
redis>=4.6.0
aiogram>=3.4.0
python-telegram-bot>=20.4
bcrypt>=4.0.1
redis>=4.0.0
python-dotenv==1.0.1
psycopg2-binary==2.9.9
alembic==1.13.1
pytest==8.0.0
httpx==0.26.0
requests>=2.26.0
aiogram==3.4.1

Binary file not shown.

View File

@@ -1,10 +1,127 @@
"""Test configuration"""
"""Test configuration."""
import os
import pytest
from typing import Generator
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from unittest.mock import Mock, patch
from app.database import Base, get_db
from app.main import app
from app.models.employee import Employee
from app.utils.auth import get_password_hash
from app.utils.jwt import create_access_token
from app.core.config import settings
@pytest.fixture
def client():
"""Test client fixture"""
return TestClient(app)
# Создаем тестовую базу данных в памяти
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class MockRedis:
"""Мок для Redis."""
def __init__(self):
self.data = {}
def get(self, key):
return self.data.get(key)
def set(self, key, value, ex=None):
self.data[key] = value
return True
def delete(self, key):
if key in self.data:
del self.data[key]
return True
def exists(self, key):
return key in self.data
@pytest.fixture(scope="function")
def redis_mock():
"""Фикстура для мока Redis."""
with patch("app.utils.jwt.redis") as mock:
redis_instance = MockRedis()
mock.get.side_effect = redis_instance.get
mock.set.side_effect = redis_instance.set
mock.delete.side_effect = redis_instance.delete
mock.exists.side_effect = redis_instance.exists
yield mock
@pytest.fixture(scope="function")
def db() -> Generator:
"""Фикстура для создания тестовой базы данных."""
Base.metadata.create_all(bind=engine)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
Base.metadata.drop_all(bind=engine)
@pytest.fixture(scope="function")
def client(db: TestingSessionLocal, redis_mock) -> Generator:
"""Фикстура для создания тестового клиента."""
def override_get_db():
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
return TestClient(app)
@pytest.fixture(scope="function")
def test_employee(db: TestingSessionLocal) -> Employee:
"""Фикстура для создания тестового сотрудника."""
employee = Employee(
email="test@example.com",
full_name="Test Employee",
hashed_password=get_password_hash("testpassword"),
is_active=True,
is_admin=False,
department="IT"
)
db.add(employee)
db.commit()
db.refresh(employee)
return employee
@pytest.fixture(scope="function")
def test_admin(db: TestingSessionLocal) -> Employee:
"""Фикстура для создания тестового администратора."""
admin = Employee(
email="admin@example.com",
full_name="Test Admin",
hashed_password=get_password_hash("adminpassword"),
is_active=True,
is_admin=True,
department="Administration"
)
db.add(admin)
db.commit()
db.refresh(admin)
return admin
@pytest.fixture(scope="function")
def employee_token(test_employee: Employee, db: TestingSessionLocal) -> str:
"""Фикстура для создания токена тестового сотрудника."""
from app.utils.jwt import create_access_token
token = create_access_token({"sub": str(test_employee.id)})
# Сохраняем токен в Redis мок
from app.utils.jwt import redis
redis.set(f"token:{token}", "valid")
return token
@pytest.fixture(scope="function")
def admin_token(test_admin: Employee, db: TestingSessionLocal) -> str:
"""Фикстура для создания токена администратора."""
from app.utils.jwt import create_access_token
token = create_access_token({"sub": str(test_admin.id)})
# Сохраняем токен в Redis мок
from app.utils.jwt import redis
redis.set(f"token:{token}", "valid")
return token

View File

@@ -0,0 +1,80 @@
"""Authentication tests."""
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.models.employee import Employee
def test_login_employee_success(client: TestClient, test_employee: Employee):
"""Тест успешной авторизации сотрудника."""
response = client.post(
"/api/auth/login",
data={"username": test_employee.email, "password": "testpassword"}
)
assert response.status_code == 200
assert "access_token" in response.json()
assert "token_type" in response.json()
assert response.json()["token_type"] == "bearer"
def test_login_employee_wrong_password(client: TestClient, test_employee: Employee):
"""Тест авторизации сотрудника с неверным паролем."""
response = client.post(
"/api/auth/login",
data={"username": test_employee.email, "password": "wrongpassword"}
)
assert response.status_code == 401
assert response.json()["detail"] == "Incorrect username or password"
def test_login_employee_wrong_username(client: TestClient):
"""Тест авторизации с несуществующим пользователем."""
response = client.post(
"/api/auth/login",
data={"username": "nonexistent@example.com", "password": "testpassword"}
)
assert response.status_code == 401
assert response.json()["detail"] == "Incorrect username or password"
def test_login_admin_success(client: TestClient, test_admin: Employee):
"""Тест успешной авторизации администратора."""
response = client.post(
"/api/auth/admin/login",
data={"username": test_admin.email, "password": "adminpassword"}
)
assert response.status_code == 200
assert "access_token" in response.json()
assert "token_type" in response.json()
assert response.json()["token_type"] == "bearer"
def test_login_admin_wrong_password(client: TestClient, test_admin: Employee):
"""Тест авторизации администратора с неверным паролем."""
response = client.post(
"/api/auth/admin/login",
data={"username": test_admin.email, "password": "wrongpassword"}
)
assert response.status_code == 401
assert response.json()["detail"] == "Incorrect username or password"
def test_protected_route_with_valid_token(client: TestClient, employee_token: str, test_employee: Employee, db: Session):
"""Тест доступа к защищенному маршруту с валидным токеном."""
response = client.get(
"/api/employees/me",
headers={"Authorization": f"Bearer {employee_token}"}
)
assert response.status_code == 200
data = response.json()
assert data["email"] == test_employee.email
assert data["full_name"] == test_employee.full_name
def test_protected_route_without_token(client: TestClient):
"""Тест доступа к защищенному маршруту без токена."""
response = client.get("/api/employees/me")
assert response.status_code == 401
assert response.json()["detail"] == "Not authenticated"
def test_protected_route_with_invalid_token(client: TestClient):
"""Тест доступа к защищенному маршруту с недействительным токеном."""
response = client.get(
"/api/employees/me",
headers={"Authorization": "Bearer invalid_token"}
)
assert response.status_code == 401
assert response.json()["detail"] == "Could not validate credentials"

View File

@@ -0,0 +1,135 @@
"""Employee tests."""
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.models.employee import Employee
def test_create_employee(client: TestClient, admin_token: str, db: Session):
"""Тест создания сотрудника."""
response = client.post(
"/api/employees",
headers={"Authorization": f"Bearer {admin_token}"},
json={
"email": "new@example.com",
"password": "newpassword",
"full_name": "New Employee",
"department": "IT",
"is_active": True,
"is_admin": False
}
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "new@example.com"
assert data["full_name"] == "New Employee"
assert data["department"] == "IT"
assert "id" in data
def test_create_employee_unauthorized(client: TestClient):
"""Тест создания сотрудника без авторизации."""
response = client.post(
"/api/employees",
json={
"email": "new@example.com",
"password": "newpassword",
"full_name": "New Employee",
"is_active": True,
"is_admin": False
}
)
assert response.status_code == 401
assert response.json()["detail"] == "Not authenticated"
def test_get_employees(client: TestClient, admin_token: str, test_employee: Employee, db: Session):
"""Тест получения списка сотрудников."""
response = client.get(
"/api/employees",
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) > 0
assert "email" in data[0]
assert "full_name" in data[0]
assert "department" in data[0]
def test_get_employee_by_id(client: TestClient, admin_token: str, test_employee: Employee, db: Session):
"""Тест получения сотрудника по ID."""
response = client.get(
f"/api/employees/{test_employee.id}",
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == 200
data = response.json()
assert data["email"] == test_employee.email
assert data["full_name"] == test_employee.full_name
assert data["department"] == test_employee.department
def test_get_nonexistent_employee(client: TestClient, admin_token: str):
"""Тест получения несуществующего сотрудника."""
response = client.get(
"/api/employees/999",
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == 404
assert response.json()["detail"] == "Employee not found"
def test_update_employee(client: TestClient, admin_token: str, test_employee: Employee, db: Session):
"""Тест обновления данных сотрудника."""
response = client.put(
f"/api/employees/{test_employee.id}",
headers={"Authorization": f"Bearer {admin_token}"},
json={
"email": "updated@example.com",
"full_name": "Updated Employee",
"department": "HR",
"is_active": True,
"is_admin": False
}
)
assert response.status_code == 200
data = response.json()
assert data["email"] == "updated@example.com"
assert data["full_name"] == "Updated Employee"
assert data["department"] == "HR"
def test_delete_employee(client: TestClient, admin_token: str, test_employee: Employee, db: Session):
"""Тест удаления сотрудника."""
response = client.delete(
f"/api/employees/{test_employee.id}",
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == 200
data = response.json()
assert data["email"] == test_employee.email
assert data["full_name"] == test_employee.full_name
assert data["department"] == test_employee.department
def test_employee_me(client: TestClient, employee_token: str, test_employee: Employee, db: Session):
"""Тест получения информации о текущем сотруднике."""
response = client.get(
"/api/employees/me",
headers={"Authorization": f"Bearer {employee_token}"}
)
assert response.status_code == 200
data = response.json()
assert data["email"] == test_employee.email
assert data["full_name"] == test_employee.full_name
assert data["department"] == test_employee.department
def test_update_me(client: TestClient, employee_token: str, test_employee: Employee, db: Session):
"""Тест обновления информации о текущем сотруднике."""
response = client.put(
"/api/employees/me",
headers={"Authorization": f"Bearer {employee_token}"},
json={
"full_name": "Updated Name",
"department": "Support"
}
)
assert response.status_code == 200
data = response.json()
assert data["full_name"] == "Updated Name"
assert data["email"] == test_employee.email
assert data["department"] == "Support"

View File

@@ -0,0 +1,168 @@
"""Request tests."""
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.models.employee import Employee
from app.models.request import Request
def test_create_request(client: TestClient, employee_token: str, db: Session):
"""Тест создания заявки."""
response = client.post(
"/api/requests",
headers={"Authorization": f"Bearer {employee_token}"},
json={
"request_type": "support",
"description": "Test Description",
"priority": "medium"
}
)
assert response.status_code == 201
data = response.json()
assert data["request_type"] == "support"
assert data["description"] == "Test Description"
assert data["priority"] == "medium"
assert data["status"] == "new"
assert "id" in data
def test_create_request_unauthorized(client: TestClient):
"""Тест создания заявки без авторизации."""
response = client.post(
"/api/requests",
json={
"request_type": "support",
"description": "Test Description",
"priority": "medium"
}
)
assert response.status_code == 401
assert response.json()["detail"] == "Not authenticated"
def test_get_employee_requests(client: TestClient, employee_token: str, test_employee: Employee, db: Session):
"""Тест получения списка заявок сотрудника."""
db.add(test_employee)
db.commit()
db.refresh(test_employee)
# Создаем тестовую заявку
request = Request(
request_type="support",
description="Test Description",
priority="medium",
status="new",
employee_id=test_employee.id
)
db.add(request)
db.commit()
response = client.get(
"/api/requests/my",
headers={"Authorization": f"Bearer {employee_token}"}
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) > 0
assert data[0]["request_type"] == "support"
assert data[0]["description"] == "Test Description"
def test_admin_get_all_requests(client: TestClient, admin_token: str, test_employee: Employee, db: Session):
"""Тест получения всех заявок администратором."""
db.add(test_employee)
db.commit()
db.refresh(test_employee)
# Создаем тестовую заявку
request = Request(
request_type="support",
description="Test Description",
priority="medium",
status="new",
employee_id=test_employee.id
)
db.add(request)
db.commit()
response = client.get(
"/api/requests/admin",
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) > 0
assert data[0]["request_type"] == "support"
assert data[0]["description"] == "Test Description"
def test_update_request_status(client: TestClient, admin_token: str, test_employee: Employee, db: Session):
"""Тест обновления статуса заявки."""
db.add(test_employee)
db.commit()
db.refresh(test_employee)
# Создаем тестовую заявку
request = Request(
request_type="support",
description="Test Description",
priority="medium",
status="new",
employee_id=test_employee.id,
department=test_employee.department
)
db.add(request)
db.commit()
response = client.patch(
f"/api/requests/{request.id}/status",
headers={"Authorization": f"Bearer {admin_token}"},
json={"status": "in_progress"}
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "in_progress"
def test_get_request_statistics(client: TestClient, admin_token: str, test_employee: Employee, db: Session):
"""Тест получения статистики по заявкам."""
db.add(test_employee)
db.commit()
db.refresh(test_employee)
# Создаем тестовые заявки с разными статусами
requests = [
Request(
request_type="support",
description="Test Description",
priority="medium",
status="new",
employee_id=test_employee.id,
department=test_employee.department
),
Request(
request_type="support",
description="Test Description",
priority="high",
status="in_progress",
employee_id=test_employee.id,
department=test_employee.department
),
Request(
request_type="support",
description="Test Description",
priority="low",
status="completed",
employee_id=test_employee.id,
department=test_employee.department
)
]
for req in requests:
db.add(req)
db.commit()
response = client.get(
"/api/statistics",
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == 200
data = response.json()
assert "total" in data
assert "by_status" in data
assert data["total"] >= 3

View File

@@ -1,22 +1,42 @@
version: '3.8'
services:
postgres:
image: postgres:15
container_name: postgres
backend:
image: backend:latest
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- ./backend/docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
- postgres_data:/var/lib/postgresql/data
- DATABASE_URL=postgresql://postgres:postgres@db:5432/app
- REDIS_HOST=redis
- REDIS_PORT=6379
- SECRET_KEY=${SECRET_KEY}
depends_on:
- db
- redis
frontend:
image: frontend:latest
restart: always
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
- "80:80"
depends_on:
- backend
db:
image: postgres:15
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=app
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:latest
restart: always
volumes:
- redis_data:/data
volumes:
postgres_data:
postgres_data:
redis_data:

View File

@@ -1,7 +1,6 @@
# Build stage
FROM node:18 as build-stage
FROM node:18-alpine AS build
# Set working directory
WORKDIR /app
# Copy package files
@@ -10,25 +9,29 @@ COPY package*.json ./
# Install dependencies
RUN npm install
# Copy project files
# Copy source code
COPY . .
# Install Vue dependencies
RUN npm install vue@latest @vitejs/plugin-vue vue-tsc typescript
# Install Vue compiler globally
RUN npm install -g @vue/compiler-sfc
# Set environment variables
ENV NODE_ENV=production
ENV VITE_API_URL=/api
# Build the application with increased memory limit
RUN NODE_OPTIONS="--max-old-space-size=4096" npm run build
# Production stage
FROM nginx:stable-alpine as production-stage
FROM nginx:alpine
# Copy built files
COPY --from=build-stage /app/dist /usr/share/nginx/html
# Copy built files from build stage
COPY --from=build /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
# Expose port 80
EXPOSE 80
# Start nginx

View File

@@ -6,8 +6,7 @@
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test": "echo \"No tests configured yet\" && exit 0"
"preview": "vite preview"
},
"dependencies": {
"@vueuse/core": "^10.9.0",

View File

@@ -3,7 +3,7 @@
<div class="max-w-md w-full bg-white rounded-xl shadow-2xl p-8">
<div class="text-center mb-8">
<h2 class="text-3xl font-bold text-gray-900">
Панель администратора
Панель администратора 55
</h2>
<p class="mt-2 text-gray-600">
Вход в систему управления

View File

@@ -9,26 +9,23 @@ export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@': path.resolve(__dirname, './src'),
},
},
build: {
chunkSizeWarningLimit: 1600,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor';
}
}
}
}
optimizeDeps: {
include: ['axios']
},
server: {
host: true,
port: 5173,
watch: {
usePolling: true
},
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
secure: false
}
}
}

View File

@@ -1,4 +1,4 @@
#!/bin/bashsss
#!/bin/bash
# Остановить все контейнеры
docker compose down -v

View File

@@ -0,0 +1,2 @@
# Попытка вывести неопределенную переменную
print(my_var) # Вызовет NameError