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: variables:
SECRET_KEY: "your-super-secret-key-123" SECRET_KEY: "your-super-secret-key-123"
test-backend: test:
image: python:3.11 image: python:3.11
stage: test stage: test
before_script: before_script:
- python -V - python -V
- python -m pip install --upgrade pip - python -m pip install --upgrade pip
- pip install pytest pytest-cov - pip install pytest
- pip install -r backend/requirements.txt - pip install -r backend/requirements.txt
script: script:
- cd backend - cd backend
- python -m pytest -v tests/test_health.py - python -m pytest -v
only: only:
- main - main
- Testing - Testing
test-frontend: build_backend:
image: node:18
stage: test
before_script:
- cd frontend
- npm install
script:
- npm run test
only:
- main
- Testing
build-backend:
stage: build stage: build
image: docker:latest image: docker:latest
variables: variables:
@@ -57,7 +45,7 @@ build-backend:
only: only:
- main - main
build-frontend: build_frontend:
stage: build stage: build
image: docker:latest image: docker:latest
variables: 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) [![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""" """Settings configuration"""
import os from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic_settings import BaseSettings
class Settings(BaseSettings): class Settings(BaseSettings):
"""Application settings""" """Application settings"""
PROJECT_NAME: str = "Employee Request System" PROJECT_NAME: str = "Support Service"
VERSION: str = "1.0.0"
API_V1_STR: str = "/api" API_V1_STR: str = "/api"
# Database # Database
POSTGRES_USER: str = os.getenv("POSTGRES_USER", "postgres") DATABASE_URL: str = "postgresql://postgres:postgres123@db:5432/support_db"
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")
# JWT # JWT
SECRET_KEY: str = os.getenv("SECRET_KEY", "your-secret-key-super-secret") SECRET_KEY: str = "your-secret-key"
ALGORITHM: str = "HS256" ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# Redis # Redis
REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost") REDIS_HOST: str = "redis"
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379")) REDIS_PORT: int = 6379
REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
# Telegram # Admin
TELEGRAM_BOT_TOKEN: str = os.getenv("TELEGRAM_BOT_TOKEN", "") ADMIN_USERNAME: str = "admin"
TELEGRAM_CHAT_ID: str = os.getenv("TELEGRAM_CHAT_ID", "") ADMIN_PASSWORD: str = "admin123"
model_config = {
"case_sensitive": True,
"env_file": ".env",
"extra": "allow"
}
def get_database_url(self) -> str: # Telegram
"""Get database URL""" TELEGRAM_BOT_TOKEN: str = "your-bot-token"
return os.getenv( TELEGRAM_CHAT_ID: str = "your-chat-id"
"DATABASE_URL",
f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}" model_config = SettingsConfigDict(
) env_file=".env",
env_file_encoding="utf-8",
case_sensitive=True
)
settings = Settings() settings = Settings()

View File

@@ -1,6 +1,6 @@
"""Employee CRUD operations""" """Employee CRUD operations"""
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List, Optional from typing import Optional, List
from ..models.employee import Employee from ..models.employee import Employee
from ..schemas.employee import EmployeeCreate, EmployeeUpdate from ..schemas.employee import EmployeeCreate, EmployeeUpdate
from ..utils.loggers import auth_logger from ..utils.loggers import auth_logger
@@ -13,42 +13,58 @@ def get_employee(db: Session, employee_id: int) -> Optional[Employee]:
"""Get employee by ID""" """Get employee by ID"""
return db.query(Employee).filter(Employee.id == employee_id).first() 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]: def get_employee_by_email(db: Session, email: str) -> Optional[Employee]:
"""Get employee by first name and last name""" """Get employee by email"""
return db.query(Employee).filter( return db.query(Employee).filter(Employee.email == email).first()
Employee.first_name == first_name,
Employee.last_name == last_name
).first()
def create_employee(db: Session, employee: EmployeeCreate, hashed_password: str) -> Employee: def create_employee(db: Session, employee: EmployeeCreate, hashed_password: str) -> Employee:
"""Create new employee""" """Create new employee"""
db_employee = Employee( try:
first_name=employee.first_name, db_employee = Employee(
last_name=employee.last_name, email=employee.email,
department=employee.department, full_name=employee.full_name,
office=employee.office, hashed_password=hashed_password,
hashed_password=hashed_password, is_active=employee.is_active,
is_admin=employee.is_admin is_admin=employee.is_admin,
) department=employee.department
db.add(db_employee) )
db.commit() db.add(db_employee)
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)
db.commit() db.commit()
db.refresh(db_employee) 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]: def delete_employee(db: Session, employee_id: int) -> Optional[Employee]:
"""Delete employee""" """Delete employee"""
db_employee = get_employee(db, employee_id) db_employee = get_employee(db, employee_id)
if db_employee: if db_employee:
db.delete(db_employee) try:
db.commit() db.delete(db_employee)
return 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: def create_request(db: Session, request: RequestCreate, employee_id: int) -> Request:
"""Create new request""" """Create new request"""
# Получаем данные сотрудника
employee = employees.get_employee(db, employee_id)
if not employee:
raise ValueError("Employee not found")
db_request = Request( db_request = Request(
department=employee.department, # Берем отдел из данных сотрудника
request_type=request.request_type, request_type=request.request_type,
description=request.description, description=request.description,
priority=request.priority, priority=request.priority,
@@ -24,6 +30,27 @@ def get_request(db: Session, request_id: int) -> Optional[Request]:
"""Get request by ID""" """Get request by ID"""
return db.query(Request).filter(Request.id == request_id).first() 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]: def get_employee_requests(db: Session, employee_id: int) -> list[Request]:
"""Get employee's requests""" """Get employee's requests"""
return db.query(Request).filter(Request.employee_id == employee_id).all() 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) func.count(Request.id)
).group_by(Request.status).all() ).group_by(Request.status).all()
) )
# Добавляем статусы с нулевым количеством
for status in RequestStatus:
if status not in by_status:
by_status[status] = 0
return { return {
"total": total, "total": total,
"by_status": by_status "by_status": by_status

View File

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

View File

@@ -1,34 +1,22 @@
"""Database module""" """Database configuration"""
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base from sqlalchemy.orm import sessionmaker
from typing import Generator
from .core.config import settings 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 SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL
engine = create_engine(
settings.get_database_url(),
pool_pre_ping=True,
pool_size=5,
max_overflow=10
)
# Create session factory engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db() -> Generator: def get_db():
"""Get database session"""
db = SessionLocal() db = SessionLocal()
try: try:
yield db yield db
finally: finally:
db.close() 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)

View File

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

View File

@@ -1,34 +1,40 @@
"""Main application module""" """Main application module"""
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from . import models
from .core.config import settings from .routers import admin, employees, requests, auth, statistics
from .routers import auth, employees, requests, admin
from .db.base import Base
from .database import engine
app = FastAPI( 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( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=origins,
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
expose_headers=["*"]
) )
# Health check endpoint # Include routers
@app.get("/api/health") app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
async def health_check(): app.include_router(employees.router, prefix="/api/employees", tags=["employees"])
"""Health check endpoint""" app.include_router(requests.router, prefix="/api/requests", tags=["requests"])
return {"status": "ok"} app.include_router(admin.router, prefix="/api/admin", tags=["admin"])
app.include_router(statistics.router, prefix="/api/statistics", tags=["statistics"])
# Подключаем роутеры
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"])

View File

@@ -1,6 +1,5 @@
"""Models package""" """Models initialization"""
from .employee import Employee from .employee import Employee
from .request import Request 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.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() Base = declarative_base()

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ from typing import Optional
from ..database import get_db from ..database import get_db
from ..crud import employees from ..crud import employees
from ..schemas.auth import Token, LoginCredentials from ..schemas.auth import Token
from ..utils.auth import verify_password from ..utils.auth import verify_password
from ..utils.jwt import create_and_save_token from ..utils.jwt import create_and_save_token
@@ -19,18 +19,8 @@ async def login_for_access_token(
db: Session = Depends(get_db) 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): if not employee or not verify_password(form_data.password, employee.hashed_password):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@@ -52,18 +42,8 @@ async def admin_login(
db: Session = Depends(get_db) 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): if not employee or not employee.is_admin or not verify_password(form_data.password, employee.hashed_password):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,67 +1,82 @@
"""JWT utilities""" """JWT utilities"""
from datetime import datetime, timedelta from datetime import datetime, timedelta
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from typing import Optional 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.config import settings
from ..core.test_config import test_settings
from ..models.token import Token from ..models.token import Token
from ..schemas.auth import TokenData from ..crud.employees import get_employee
def get_settings(): redis = Redis(
"""Get settings based on environment""" host=settings.REDIS_HOST,
return test_settings if test_settings.TESTING else settings port=settings.REDIS_PORT,
decode_responses=True
)
def create_access_token(data: dict) -> str: def create_access_token(data: dict) -> str:
"""Create access token"""
to_encode = data.copy() to_encode = data.copy()
config = get_settings() expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
expire = datetime.utcnow() + timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire}) 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 return encoded_jwt
def verify_token(token: str) -> Optional[int]: def verify_token(token: str, db: Session) -> dict:
"""Verify token and return employee_id"""
try: try:
config = get_settings() payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[config.ALGORITHM]) user_id: int = payload.get("sub")
employee_id = int(payload.get("sub")) if user_id is None:
if employee_id is None: raise HTTPException(
return None status_code=status.HTTP_401_UNAUTHORIZED,
return employee_id detail="Could not validate credentials",
except (JWTError, ValueError): )
return None
# Проверяем токен в 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]: def create_and_save_token(user_id: int, db: Session) -> str:
"""Verify token in database""" # Создаем JWT токен
employee_id = verify_token(token) access_token = create_access_token({"sub": str(user_id)})
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)})
# Удаляем старые токены пользователя # Сохраняем в БД
db.query(Token).filter(Token.employee_id == employee_id).delete()
# Сохраняем новый токен в базу
db_token = Token( db_token = Token(
token=access_token, token=access_token,
employee_id=employee_id user_id=user_id
) )
db.add(db_token) db.add(db_token)
db.commit() 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 from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
import asyncio import asyncio
from datetime import datetime from datetime import datetime
import os
from logging import getLogger from logging import getLogger
from ..models.request import RequestStatus, RequestPriority from ..models.request import RequestStatus, RequestPriority
from ..crud import requests from ..crud import requests
from ..database import get_db from ..database import get_db
from ..core.config import settings
# Initialize logger # Initialize logger
logger = getLogger(__name__) logger = getLogger(__name__)
# Initialize bot with token from settings # Initialize bot with token
bot = Bot(token=settings.TELEGRAM_BOT_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: def format_priority(priority: str) -> str:
"""Format priority with emoji""" """Format priority with emoji"""
@@ -56,7 +59,7 @@ async def send_request_notification(request_id: int):
) )
await bot.send_message( await bot.send_message(
chat_id=settings.TELEGRAM_CHAT_ID, chat_id=TELEGRAM_CHAT_ID,
text=message, text=message,
parse_mode="HTML" parse_mode="HTML"
) )

View File

@@ -1,79 +1,5 @@
-- Создаем основную базу данных -- Connect to the database
CREATE DATABASE app; \c support_db;
\c app;
-- Создаем таблицы для основной базы данных -- Create extensions if needed
CREATE TABLE employees ( CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
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);

View File

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

Binary file not shown.

View File

@@ -1,10 +1,127 @@
"""Test configuration""" """Test configuration."""
import os
import pytest import pytest
from typing import Generator
from fastapi.testclient import TestClient 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.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(): SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
"""Test client fixture""" engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
return TestClient(app) 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' version: '3.8'
services: services:
postgres: backend:
image: postgres:15 image: backend:latest
container_name: postgres restart: always
environment: environment:
POSTGRES_USER: postgres - DATABASE_URL=postgresql://postgres:postgres@db:5432/app
POSTGRES_PASSWORD: postgres - REDIS_HOST=redis
volumes: - REDIS_PORT=6379
- ./backend/docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql - SECRET_KEY=${SECRET_KEY}
- postgres_data:/var/lib/postgresql/data depends_on:
- db
- redis
frontend:
image: frontend:latest
restart: always
ports: ports:
- "5432:5432" - "80:80"
healthcheck: depends_on:
test: ["CMD-SHELL", "pg_isready -U postgres"] - backend
interval: 5s
timeout: 5s db:
retries: 5 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: volumes:
postgres_data: postgres_data:
redis_data:

View File

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

View File

@@ -6,8 +6,7 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"preview": "vite preview", "preview": "vite preview"
"test": "echo \"No tests configured yet\" && exit 0"
}, },
"dependencies": { "dependencies": {
"@vueuse/core": "^10.9.0", "@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="max-w-md w-full bg-white rounded-xl shadow-2xl p-8">
<div class="text-center mb-8"> <div class="text-center mb-8">
<h2 class="text-3xl font-bold text-gray-900"> <h2 class="text-3xl font-bold text-gray-900">
Панель администратора Панель администратора 55
</h2> </h2>
<p class="mt-2 text-gray-600"> <p class="mt-2 text-gray-600">
Вход в систему управления Вход в систему управления

View File

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

View File

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

View File

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