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

добавление редисаъ

This commit is contained in:
MoonTestUse1
2025-01-03 16:36:00 +06:00
parent bf0e41c997
commit 9b1af9f069
8 changed files with 255 additions and 86 deletions

View File

@@ -0,0 +1,34 @@
"""create tokens table
Revision ID: create_tokens_table
Revises:
Create Date: 2024-01-02 22:30:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'create_tokens_table'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
'tokens',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('access_token', sa.String(), nullable=False),
sa.Column('employee_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['employee_id'], ['employees.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_tokens_access_token'), 'tokens', ['access_token'], unique=True)
op.create_index(op.f('ix_tokens_id'), 'tokens', ['id'], unique=False)
def downgrade() -> None:
op.drop_index(op.f('ix_tokens_id'), table_name='tokens')
op.drop_index(op.f('ix_tokens_access_token'), table_name='tokens')
op.drop_table('tokens')

View File

@@ -0,0 +1,20 @@
from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
DATABASE_URL: str = "postgresql://postgres:postgres@db:5432/support_db"
REDIS_URL: str = "redis://redis:6379/0"
# JWT settings
JWT_SECRET_KEY: str = "your-secret-key" # в продакшене использовать сложный ключ
JWT_ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# Telegram settings
TELEGRAM_BOT_TOKEN: Optional[str] = None
TELEGRAM_CHAT_ID: Optional[str] = None
class Config:
env_file = ".env"
settings = Settings()

View File

@@ -0,0 +1,12 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.sql import func
from ..database import Base
class Token(Base):
__tablename__ = "tokens"
id = Column(Integer, primary_key=True, index=True)
access_token = Column(String, unique=True, index=True)
employee_id = Column(Integer, ForeignKey("employees.id", ondelete="CASCADE"))
created_at = Column(DateTime(timezone=True), server_default=func.now())
expires_at = Column(DateTime(timezone=True))

View File

@@ -1,50 +1,58 @@
"""Authentication router"""
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from typing import Optional
from ..database import get_db
from ..models.employee import Employee
from ..schemas.auth import AdminLogin, EmployeeLogin
from passlib.context import CryptContext
from ..crud import employees
from ..schemas.auth import Token
from ..utils.auth import verify_password
from ..utils.jwt import create_and_save_token
router = APIRouter()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@router.post("/admin/login")
def admin_login(login_data: AdminLogin, db: Session = Depends(get_db)):
"""Admin login endpoint"""
if login_data.username == "admin" and login_data.password == "admin123":
return {
"access_token": "admin_token",
"token_type": "bearer"
}
raise HTTPException(status_code=401, detail="Invalid credentials")
@router.post("/login")
def employee_login(login_data: EmployeeLogin, db: Session = Depends(get_db)):
"""Employee login endpoint"""
# Ищем сотрудника по фамилии
employee = db.query(Employee).filter(Employee.last_name == login_data.last_name).first()
if not employee:
@router.post("/token", response_model=Token)
async def login_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db)
):
# Проверяем учетные данные сотрудника
employee = employees.get_employee_by_last_name(db, form_data.username)
if not employee or not verify_password(form_data.password, employee.hashed_password):
raise HTTPException(
status_code=401,
detail="Сотрудник с такой фамилией не найден"
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Проверяем пароль
if not pwd_context.verify(login_data.password, employee.password):
raise HTTPException(
status_code=401,
detail="Неверный пароль"
)
# Создаем и сохраняем токен
access_token = create_and_save_token(employee.id, db)
# Возвращаем данные сотрудника
return {
"id": employee.id,
"first_name": employee.first_name,
"last_name": employee.last_name,
"department": employee.department,
"office": employee.office,
"access_token": f"employee_token_{employee.id}", # Добавляем токен для авторизации
"access_token": access_token,
"token_type": "bearer"
}
@router.post("/admin/token", response_model=Token)
async def admin_login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db)
):
# Проверяем учетные данные администратора
if form_data.username != "admin" or form_data.password != "admin123":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Для админа используем специальный ID
admin_id = -1
access_token = create_and_save_token(admin_id, db)
return {
"access_token": access_token,
"token_type": "bearer"
}

View File

@@ -19,4 +19,14 @@ class EmployeeResponse(BaseModel):
last_name: str
department: str
office: str
access_token: str
access_token: str
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
employee_id: int | None = None
is_admin: bool = False
model_config = ConfigDict(from_attributes=True)

86
backend/app/utils/jwt.py Normal file
View File

@@ -0,0 +1,86 @@
from datetime import datetime, timedelta
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 ..models.token import Token
from ..crud.employees import get_employee
redis = Redis.from_url(settings.REDIS_URL, decode_responses=True)
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
return encoded_jwt
def verify_token(token: str, db: Session) -> dict:
try:
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
employee_id: int = payload.get("sub")
if employee_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
)
# Проверяем токен в Redis
if not redis.get(f"token:{token}"):
# Если токена нет в Redis, проверяем в БД
db_token = db.query(Token).filter(Token.access_token == token).first()
if not db_token or db_token.expires_at < datetime.utcnow():
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired or is invalid",
)
# Если токен валиден, кэшируем его в Redis
redis.setex(
f"token:{token}",
timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES),
"valid"
)
return payload
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
)
def create_and_save_token(employee_id: int, db: Session) -> str:
# Создаем JWT токен
access_token = create_access_token({"sub": str(employee_id)})
expires_at = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
# Сохраняем в БД
db_token = Token(
access_token=access_token,
employee_id=employee_id,
expires_at=expires_at
)
db.add(db_token)
db.commit()
# Кэшируем в 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"))
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

@@ -1,17 +1,11 @@
fastapi==0.110.0
uvicorn==0.27.1
sqlalchemy==2.0.27
pydantic==2.6.3
pydantic-settings==2.2.1
python-multipart==0.0.9
python-jose[cryptography]==3.3.0
passlib[bcrypt]>=1.7.4
bcrypt>=4.0.1
aiogram>=3.4.1
python-dotenv==1.0.1
psycopg2-binary==2.9.9
alembic==1.13.1
pytest==8.0.0
httpx==0.26.0
pytest-asyncio==0.23.5
pytest-cov==4.1.0
fastapi>=0.68.0,<0.69.0
uvicorn>=0.15.0,<0.16.0
sqlalchemy>=1.4.23,<1.5.0
pydantic>=1.8.0,<2.0.0
python-jose[cryptography]>=3.3.0,<4.0.0
passlib[bcrypt]>=1.7.4,<2.0.0
python-multipart>=0.0.5,<0.0.6
redis>=4.0.0,<5.0.0
python-dotenv>=0.19.0,<0.20.0
psycopg2-binary>=2.9.1,<3.0.0
requests>=2.26.0,<3.0.0

View File

@@ -1,47 +1,52 @@
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: support-postgres
environment:
POSTGRES_DB: support_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres123
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- support-network
backend:
build:
context: .
dockerfile: docker/backend/Dockerfile
container_name: support-backend
depends_on:
- postgres
networks:
- support-network
frontend:
container_name: support-frontend
build:
context: .
dockerfile: docker/frontend/Dockerfile
container_name: support-frontend
ports:
- "80:80"
- "443:443"
volumes:
- /etc/letsencrypt:/etc/letsencrypt:ro
- /var/www/certbot:/var/www/certbot:ro
- ./certbot/www:/var/www/certbot/:ro
- ./certbot/conf/:/etc/letsencrypt/:ro
depends_on:
- backend
networks:
- support-network
networks:
support-network:
driver: bridge
backend:
container_name: support-backend
build:
context: .
dockerfile: docker/backend/Dockerfile
volumes:
- ./backend:/app
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/support_db
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
db:
container_name: support-db
image: postgres:15
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=support_db
redis:
container_name: support-redis
image: redis:alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
postgres_data:
postgres_data:
redis_data: