From 77092e85170fef0c0b2346f37760efc9e247d7b9 Mon Sep 17 00:00:00 2001 From: Polina Date: Thu, 2 Jul 2026 17:07:38 +0000 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?app=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__init__.py | Bin 0 -> 1024 bytes app/auth.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ app/config.py | 14 +++++++++ app/database.py | 22 ++++++++++++++ app/main.py | 25 ++++++++++++++++ 5 files changed, 138 insertions(+) create mode 100644 app/__init__.py create mode 100644 app/auth.py create mode 100644 app/config.py create mode 100644 app/database.py create mode 100644 app/main.py diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..06d7405020018ddf3cacee90fd4af10487da3d20 GIT binary patch literal 1024 ScmZQz7zLvtFd70QH3R?z00031 literal 0 HcmV?d00001 diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..d861049 --- /dev/null +++ b/app/auth.py @@ -0,0 +1,77 @@ +import secrets +from datetime import datetime, timedelta + +import bcrypt +from fastapi import Depends, HTTPException, Request, status +from sqlalchemy.orm import Session as DbSession + +from app.config import settings +from app.database import get_db +from app.models import Session, User + +SESSION_COOKIE = "session_token" + + +def hash_password(password: str) -> str: + return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() + + +def verify_password(plain: str, hashed: str) -> bool: + return bcrypt.checkpw(plain.encode(), hashed.encode()) + + +def create_session_token() -> str: + return secrets.token_urlsafe(32) + + +def create_user_session(db: DbSession, user: User) -> Session: + token = create_session_token() + expires_at = datetime.utcnow() + timedelta(hours=settings.session_lifetime_hours) + session = Session(user_id=user.id, token=token, expires_at=expires_at) + db.add(session) + db.commit() + db.refresh(session) + return session + + +def get_session_by_token(db: DbSession, token: str) -> Session | None: + session = db.query(Session).filter(Session.token == token, Session.is_active.is_(True)).first() + if not session: + return None + if session.expires_at < datetime.utcnow(): + session.is_active = False + db.commit() + return None + return session + + +def get_current_user(request: Request, db: DbSession = Depends(get_db)) -> User: + token = request.cookies.get(SESSION_COOKIE) + if not token: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") + session = get_session_by_token(db, token) + if not session: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Session expired") + user = db.query(User).filter(User.id == session.user_id).first() + if not user: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found") + return user + + +def get_current_user_optional(request: Request, db: DbSession = Depends(get_db)) -> User | None: + token = request.cookies.get(SESSION_COOKIE) + if not token: + return None + session = get_session_by_token(db, token) + if not session: + return None + return db.query(User).filter(User.id == session.user_id).first() + + +def verify_api_key(request: Request) -> None: + auth = request.headers.get("Authorization", "") + if not auth.startswith("Bearer "): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key") + key = auth.removeprefix("Bearer ").strip() + if not secrets.compare_digest(key, settings.api_key): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid API key") diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..496c8a1 --- /dev/null +++ b/app/config.py @@ -0,0 +1,14 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + secret_key: str = "change-me-in-production-use-env-var" + database_url: str = "sqlite:///./robot_management.db" + session_lifetime_hours: int = 24 + api_key: str = "demo-api-key-change-in-production" + + class Config: + env_file = ".env" + + +settings = Settings() diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..f797dd9 --- /dev/null +++ b/app/database.py @@ -0,0 +1,22 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import DeclarativeBase, sessionmaker + +from app.config import settings + +engine = create_engine( + settings.database_url, + connect_args={"check_same_thread": False}, +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +class Base(DeclarativeBase): + pass + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..eeb7ac8 --- /dev/null +++ b/app/main.py @@ -0,0 +1,25 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles + +from app.database import Base, engine +from app.routers import api, web + + +@asynccontextmanager +async def lifespan(app: FastAPI): + Base.metadata.create_all(bind=engine) + yield + + +app = FastAPI( + title="Robot Management System", + description="Прототип веб-системы управления роботами — личный кабинет и API", + version="1.0.0", + lifespan=lifespan, +) + +app.mount("/static", StaticFiles(directory="static"), name="static") +app.include_router(web.router) +app.include_router(api.router)