mirror of
https://gitlab.com/MoonTestUse1/AdministrationItDepartmens.git
synced 2025-08-14 00:25:46 +02:00
добавление редисаъ
This commit is contained in:
34
backend/alembic/versions/create_tokens_table.py
Normal file
34
backend/alembic/versions/create_tokens_table.py
Normal 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')
|
20
backend/app/core/config.py
Normal file
20
backend/app/core/config.py
Normal 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()
|
12
backend/app/models/token.py
Normal file
12
backend/app/models/token.py
Normal 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))
|
@@ -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"
|
||||
}
|
@@ -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
86
backend/app/utils/jwt.py
Normal 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
|
@@ -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
|
@@ -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:
|
Reference in New Issue
Block a user