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

Fix database

This commit is contained in:
MoonTestUse1
2025-01-07 05:26:33 +06:00
parent d9e276ad6b
commit bdf4ae9d70
29 changed files with 727 additions and 531 deletions

View File

@@ -1,25 +1,55 @@
"""Application configuration"""
from functools import lru_cache
import os
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
"""Application settings"""
PROJECT_NAME: str = "Employee Request System"
# Database
DATABASE_URL: str = "postgresql://postgres:postgres@localhost:5432/employee_requests"
# База данных
POSTGRES_USER: str = "postgres"
POSTGRES_PASSWORD: str = "postgres"
POSTGRES_HOST: str = "postgres"
POSTGRES_PORT: str = "5432"
POSTGRES_DB: str = "app"
POSTGRES_TEST_DB: str = "test_app"
DATABASE_URL: str | None = None
# JWT
SECRET_KEY: str = "your-secret-key"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# Режим тестирования
TESTING: bool = bool(os.getenv("TESTING"))
# Redis
REDIS_HOST: str = "localhost"
REDIS_HOST: str = "redis"
REDIS_PORT: int = 6379
REDIS_DB: int = 0
REDIS_TEST_DB: int = 1
def get_database_url(self) -> str:
"""Get database URL"""
if self.DATABASE_URL:
return self.DATABASE_URL
if self.TESTING:
return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@localhost:5432/{self.POSTGRES_TEST_DB}"
return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
def get_redis_url(self) -> str:
"""Get Redis URL"""
db = self.REDIS_TEST_DB if self.TESTING else self.REDIS_DB
host = "localhost" if self.TESTING else self.REDIS_HOST
return f"redis://{host}:{self.REDIS_PORT}/{db}"
# Telegram
TELEGRAM_BOT_TOKEN: str = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID: str = os.getenv("TELEGRAM_CHAT_ID", "")
class Config:
"""Pydantic config"""
env_file = ".env"
case_sensitive = True
@lru_cache()

View File

@@ -6,7 +6,12 @@ class TestSettings(BaseSettings):
PROJECT_NAME: str = "Employee Request System Test"
# Database
DATABASE_URL: str = "sqlite:///:memory:"
POSTGRES_USER: str = "postgres"
POSTGRES_PASSWORD: str = "postgres"
POSTGRES_HOST: str = "localhost"
POSTGRES_PORT: str = "5432"
POSTGRES_DB: str = "test_app"
DATABASE_URL: str = "postgresql://postgres:postgres@localhost:5432/test_app"
# JWT
SECRET_KEY: str = "test_secret_key"
@@ -16,9 +21,14 @@ class TestSettings(BaseSettings):
# Redis
REDIS_HOST: str = "localhost"
REDIS_PORT: int = 6379
REDIS_DB: int = 1
# Testing
TESTING: bool = True
class Config:
"""Pydantic config"""
case_sensitive = True
env_file = ".env.test"
test_settings = TestSettings()

View File

@@ -1,6 +1,6 @@
"""Employee CRUD operations"""
from sqlalchemy.orm import Session
from typing import Optional, List
from typing import List, Optional
from ..models.employee import Employee
from ..schemas.employee import EmployeeCreate, EmployeeUpdate
from ..utils.loggers import auth_logger
@@ -13,57 +13,42 @@ 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_last_name(db: Session, last_name: str) -> Optional[Employee]:
"""Get employee by last name"""
return db.query(Employee).filter(Employee.last_name == last_name).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 create_employee(db: Session, employee: EmployeeCreate, hashed_password: str) -> Employee:
"""Create new employee"""
try:
db_employee = Employee(
first_name=employee.first_name,
last_name=employee.last_name,
department=employee.department,
office=employee.office,
hashed_password=hashed_password
)
db.add(db_employee)
db.commit()
db.refresh(db_employee)
return db_employee
except Exception as e:
auth_logger.error(f"Error creating employee: {e}")
db.rollback()
raise
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"""
"""Update employee data"""
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:
if db_employee:
for key, value in employee.dict(exclude_unset=True).items():
setattr(db_employee, key, value)
db.commit()
db.refresh(db_employee)
return db_employee
except Exception as e:
auth_logger.error(f"Error updating employee: {e}")
db.rollback()
raise
return db_employee
def delete_employee(db: Session, employee_id: int) -> Optional[Employee]:
"""Delete employee"""
db_employee = get_employee(db, employee_id)
if 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
db.delete(db_employee)
db.commit()
return db_employee

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, user_id: int) -> Token:
def create_token(db: Session, token: str, employee_id: int) -> Token:
"""Create new token"""
db_token = Token(token=token, user_id=user_id)
db_token = Token(token=token, employee_id=employee_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_user_tokens(db: Session, user_id: int) -> bool:
"""Delete all tokens for a user"""
db.query(Token).filter(Token.user_id == user_id).delete()
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()
db.commit()
return True

View File

@@ -1,25 +1,24 @@
"""Database module"""
"""Database configuration"""
import os
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from .core.config import settings
# Определяем URL базы данных в зависимости от окружения
if os.getenv("TESTING"):
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
else:
SQLALCHEMY_DATABASE_URL = "postgresql://postgres:postgres@postgres:5432/app"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
# Определяем, используем ли тестовую базу данных
TESTING = os.getenv("TESTING", "False") == "True"
DATABASE_URL = "sqlite:///:memory:" if TESTING else settings.DATABASE_URL
# Создаем базовый класс для моделей
Base = declarative_base()
# Создаем движок базы данных
connect_args = {"check_same_thread": False} if TESTING else {}
engine = create_engine(DATABASE_URL, connect_args=connect_args)
# Создаем фабрику сессий
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
"""Get database session"""
db = SessionLocal()

View File

@@ -1,21 +1,22 @@
"""Database initialization script"""
"""Database initialization"""
from sqlalchemy.orm import Session
from app.core.config import settings
from app.models.employee import Employee
from app.utils.auth import get_password_hash
from ..models.employee import Employee
from ..utils.auth import get_password_hash
def init_db(db: Session) -> None:
"""Initialize database with default data"""
# Создаем тестового сотрудника
test_employee = db.query(Employee).filter(Employee.last_name == "User").first()
if not test_employee:
test_employee = Employee(
first_name="Test",
# Проверяем, есть ли уже админ в базе
admin = db.query(Employee).filter(Employee.is_admin == True).first()
if not admin:
# Создаем админа по умолчанию
admin = Employee(
first_name="Admin",
last_name="User",
department="IT",
office="101",
hashed_password=get_password_hash("testpass123")
office="102",
hashed_password=get_password_hash("adminpass123"),
is_admin=True
)
db.add(test_employee)
db.add(admin)
db.commit()
db.refresh(test_employee)
db.refresh(admin)

View File

@@ -0,0 +1,63 @@
"""Dependencies module"""
from typing import Generator, Any
from sqlalchemy.orm import Session
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from .database import SessionLocal
from .core.config import settings
from .utils.jwt import verify_token
from .models.employee import Employee
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login")
def get_db() -> Generator[Session, Any, None]:
"""Get database session"""
db = SessionLocal()
try:
yield db
finally:
db.close()
async def get_current_employee(
db: Session = Depends(get_db),
token: str = Depends(oauth2_scheme)
) -> Employee:
"""Get current employee"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
employee_id = verify_token(token)
if not employee_id:
raise credentials_exception
employee = db.query(Employee).filter(Employee.id == employee_id).first()
if not employee:
raise credentials_exception
return employee
async def get_current_active_employee(
current_employee: Employee = Depends(get_current_employee),
) -> Employee:
"""Get current active employee"""
if not current_employee.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive employee"
)
return current_employee
async def get_current_admin(
current_employee: Employee = Depends(get_current_employee),
) -> Employee:
"""Get current admin"""
if not current_employee.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="The employee doesn't have enough privileges"
)
return current_employee

View File

@@ -1,62 +1,44 @@
"""Main application module"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from pydantic_settings import BaseSettings
import logging
from .models.base import Base
from .database import engine, SessionLocal
from .routers import admin, employees, requests, auth, statistics
from .routers import auth, employees, requests, admin
from .database import engine, Base
from .db.init_db import init_db
from .core.config import settings
from .database import get_db
def get_application(app_settings: BaseSettings = settings) -> FastAPI:
"""Создание экземпляра приложения с заданными настройками."""
# Создаем таблицы
Base.metadata.create_all(bind=engine)
# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Инициализируем базу данных
db = SessionLocal()
# Создаем таблицы
Base.metadata.create_all(bind=engine)
# Создаем приложение
app = FastAPI(title="Employee Request System API")
# Настраиваем CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Подключаем роутеры
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.on_event("startup")
async def startup_event():
"""Initialize database on startup"""
db = next(get_db())
try:
init_db(db)
finally:
db.close()
app = FastAPI(
# Включаем автоматическое перенаправление со слэшем
redirect_slashes=True,
# Добавляем описание API
title="Support System API",
description="API для системы поддержки",
version="1.0.0"
)
# 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=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["*"]
)
# 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"])
return app
app = get_application()
db.close()

View File

@@ -1,5 +1,6 @@
"""Token model"""
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from ..database import Base
@@ -10,5 +11,7 @@ class Token(Base):
id = Column(Integer, primary_key=True, index=True)
token = Column(String, unique=True, index=True)
employee_id = Column(Integer)
created_at = Column(DateTime, default=datetime.utcnow)
employee_id = Column(Integer, ForeignKey("employees.id"), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
employee = relationship("Employee", backref="tokens")

View File

@@ -6,7 +6,7 @@ from typing import Optional
from ..database import get_db
from ..crud import employees
from ..schemas.auth import Token
from ..schemas.auth import Token, LoginCredentials
from ..utils.auth import verify_password
from ..utils.jwt import create_and_save_token
@@ -19,8 +19,18 @@ 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_last_name(db, form_data.username)
employee = employees.get_employee_by_credentials(db, first_name, last_name)
if not employee or not verify_password(form_data.password, employee.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -42,8 +52,18 @@ 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_last_name(db, form_data.username)
employee = employees.get_employee_by_credentials(db, first_name, last_name)
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,9 +1,10 @@
"""Schemas package"""
from .employee import Employee, EmployeeCreate, EmployeeUpdate
from .request import Request, RequestCreate, RequestUpdate
from .auth import Token, TokenData
from .auth import Token, TokenData, LoginCredentials
__all__ = [
'Employee', 'EmployeeCreate', 'EmployeeUpdate',
'Request', 'RequestCreate', 'RequestUpdate',
'Token', 'TokenData'
'Token', 'TokenData', 'LoginCredentials'
]

View File

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

View File

@@ -1,31 +1,33 @@
"""Employee schemas"""
from pydantic import BaseModel, ConfigDict
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
class EmployeeBase(BaseModel):
"""Base employee schema"""
first_name: str
last_name: str
department: str
office: str
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
department: Optional[str] = None
office: Optional[str] = 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
model_config = ConfigDict(from_attributes=True)
class Config:
"""Pydantic config"""
from_attributes = True

View File

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

View File

@@ -3,11 +3,11 @@ 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")
security = HTTPBearer(auto_error=False)
@@ -23,7 +23,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
def get_current_admin(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db)
) -> dict:
) -> Employee:
"""Get current admin from token"""
if not credentials:
raise HTTPException(
@@ -34,11 +34,16 @@ def get_current_admin(
try:
token = credentials.credentials
payload = verify_token(token, db)
employee_id = int(payload.get("sub"))
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"},
)
# Проверяем, что это админ
employee = employees.get_employee(db, employee_id)
employee = employees.get_employee(db, token_data.employee_id)
if not employee or not employee.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -47,7 +52,7 @@ def get_current_admin(
)
return employee
except Exception as e:
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
@@ -57,7 +62,7 @@ def get_current_admin(
def get_current_employee(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db)
) -> dict:
) -> Employee:
"""Get current employee from token"""
if not credentials:
raise HTTPException(
@@ -68,11 +73,16 @@ def get_current_employee(
try:
token = credentials.credentials
payload = verify_token(token, db)
employee_id = int(payload.get("sub"))
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"},
)
# Проверяем существование сотрудника
employee = employees.get_employee(db, employee_id)
employee = employees.get_employee(db, token_data.employee_id)
if not employee:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,

View File

@@ -2,9 +2,11 @@
from datetime import datetime, timedelta
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from typing import Optional
from ..core.config import settings
from ..models.token import Token
from ..schemas.auth import TokenData
def create_access_token(data: dict) -> str:
"""Create access token"""
@@ -14,13 +16,22 @@ def create_access_token(data: dict) -> str:
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def verify_token(token: str, db: Session) -> dict:
def verify_token(token: str, db: Session) -> Optional[TokenData]:
"""Verify token"""
try:
# Проверяем, что токен действителен
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
except JWTError:
employee_id = int(payload.get("sub"))
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)
except (JWTError, ValueError):
return None
def create_and_save_token(employee_id: int, db: Session) -> str:

View File

@@ -3,20 +3,17 @@ 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
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)
# Initialize bot with token from settings
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
def format_priority(priority: str) -> str:
"""Format priority with emoji"""
@@ -59,7 +56,7 @@ async def send_request_notification(request_id: int):
)
await bot.send_message(
chat_id=TELEGRAM_CHAT_ID,
chat_id=settings.TELEGRAM_CHAT_ID,
text=message,
parse_mode="HTML"
)