diff --git a/backend/src/auth/__init__.py b/backend/src/auth/__init__.py deleted file mode 100644 index 8b5520d..0000000 --- a/backend/src/auth/__init__.py +++ /dev/null @@ -1,73 +0,0 @@ -import logging -from datetime import datetime, timedelta, timezone - -import jwt -from fastapi import APIRouter, Depends, HTTPException, status -from fastapi.security import OAuth2PasswordBearer -from jwt.exceptions import InvalidTokenError -from pydantic import BaseModel - -from auth.config import AuthConfig -from database import DbSessionDependency -from database.users import User - - -# TODO: evaluate FASTAPI-Users package - -class Token(BaseModel): - access_token: str - token_type: str - - -class TokenData(BaseModel): - uid: str | None = None - - -log = logging.getLogger(__name__) - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/token") - -router = APIRouter() - - -async def get_current_user(db: DbSessionDependency, token: str = Depends(oauth2_scheme)) -> User: - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - auth_config = AuthConfig - log.debug("token: " + token) - - try: - payload = jwt.decode(token, auth_config.jwt_signing_key, algorithms=[auth_config.jwt_signing_algorithm]) - log.debug("jwt payload: " + payload.__str__()) - user_uid: str = payload.get("sub") - log.debug("jwt payload sub (USER uid): " + user_uid) - if user_uid is None: - raise credentials_exception - token_data = TokenData(uid=user_uid) - except InvalidTokenError: - log.warning("received invalid token: " + token) - raise credentials_exception - - user: User | None = db.get(User, token_data.uid) - - if user is None: - log.debug("USER not found") - raise credentials_exception - - log.debug("received USER: " + user.__str__()) - return user - - -def create_access_token(data: dict, expires_delta: timedelta | None = None): - to_encode = data.copy() - auth_config = AuthConfig - if expires_delta: - expire = datetime.now(timezone.utc) + expires_delta - else: - expire = datetime.now(timezone.utc) + timedelta(minutes=auth_config.jwt_access_token_lifetime) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, auth_config.jwt_signing_key, algorithm=auth_config.jwt_signing_algorithm) - return encoded_jwt diff --git a/backend/src/auth/config.py b/backend/src/auth/config.py index 553f998..ffabe72 100644 --- a/backend/src/auth/config.py +++ b/backend/src/auth/config.py @@ -1,13 +1,12 @@ -from pydantic_settings import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict class AuthConfig(BaseSettings): # to get a signing key run: # openssl rand -hex 32 - jwt_signing_key: str - jwt_signing_algorithm: str = "HS256" - jwt_access_token_lifetime: int = 60 * 24 * 30 - + model_config = SettingsConfigDict(env_prefix='AUTH_') + token_secret: str + session_lifetime: int = 60 * 60 * 24 @property def jwt_signing_key(self): return self._jwt_signing_key diff --git a/backend/src/auth/db.py b/backend/src/auth/db.py new file mode 100644 index 0000000..d8e6580 --- /dev/null +++ b/backend/src/auth/db.py @@ -0,0 +1,34 @@ +from collections.abc import AsyncGenerator + +from fastapi import Depends +from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.orm import DeclarativeBase + +import database + + +class Base(DeclarativeBase): + pass + + +class User(SQLAlchemyBaseUserTableUUID, Base): + pass + + +engine = create_async_engine(database.db_url, echo=False) +async_session_maker = async_sessionmaker(engine, expire_on_commit=False) + + +async def create_db_and_tables(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + +async def get_async_session() -> AsyncGenerator[AsyncSession, None]: + async with async_session_maker() as session: + yield session + + +async def get_user_db(session: AsyncSession = Depends(get_async_session)): + yield SQLAlchemyUserDatabase(session, User) diff --git a/backend/src/auth/oidc.py b/backend/src/auth/oidc.py deleted file mode 100644 index a81c1bc..0000000 --- a/backend/src/auth/oidc.py +++ /dev/null @@ -1,4 +0,0 @@ -#TODO: Implement OAuth2/Open ID Connect - - - diff --git a/backend/src/auth/password.py b/backend/src/auth/password.py deleted file mode 100644 index 7e7537f..0000000 --- a/backend/src/auth/password.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Annotated - -import bcrypt -from fastapi import Depends, HTTPException, status -from fastapi.security import OAuth2PasswordRequestForm -from sqlmodel import select - -from auth import Token, create_access_token, router -from database import DbSessionDependency -from database.users import User - - -def verify_password(plain_password, hashed_password): - return bcrypt.checkpw( - bytes(plain_password, encoding="utf-8"), - bytes(hashed_password, encoding="utf-8"), - ) - - -def get_password_hash(password: str) -> str: - return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") - - -def authenticate_user(db: DbSessionDependency, email: str, password: str) -> bool | User: - """ - - :param email: email of the USER - :param password: PASSWORD of the USER - :return: if authentication succeeds, returns the USER object with added name and lastname, otherwise or if the USER doesn't exist returns False - """ - user: User | None = db.exec(select(User).where(User.email == email)).first() - if not user: - return False - if not verify_password(password, user.hashed_password): - return False - return user - - -@router.post("/token") -async def login_for_access_token( - form_data: Annotated[OAuth2PasswordRequestForm, Depends()], - db: DbSessionDependency, -) -> Token: - user = authenticate_user(db,form_data.username, form_data.password) - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect email or PASSWORD", - headers={"WWW-Authenticate": "Bearer"}, - ) - # id needs to be converted because a UUID object isn't json serializable - access_token = create_access_token(data={"sub": user.id.__str__()}) - return Token(access_token=access_token, token_type="bearer") diff --git a/backend/src/auth/schemas.py b/backend/src/auth/schemas.py new file mode 100644 index 0000000..de1169e --- /dev/null +++ b/backend/src/auth/schemas.py @@ -0,0 +1,15 @@ +import uuid + +from fastapi_users import schemas + + +class UserRead(schemas.BaseUser[uuid.UUID]): + pass + + +class UserCreate(schemas.BaseUserCreate): + pass + + +class UserUpdate(schemas.BaseUserUpdate): + pass diff --git a/backend/src/auth/users.py b/backend/src/auth/users.py new file mode 100644 index 0000000..26ed566 --- /dev/null +++ b/backend/src/auth/users.py @@ -0,0 +1,64 @@ +import uuid +from typing import Optional + +from fastapi import Depends, Request +from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models +from fastapi_users.authentication import ( + AuthenticationBackend, + BearerTransport, + CookieTransport, JWTStrategy, +) +from fastapi_users.db import SQLAlchemyUserDatabase + +import auth.config +from auth.db import User, get_user_db + +config = auth.config.AuthConfig() +SECRET = config.token_secret +LIFETIME = config.session_lifetime + + +# TODO: implement on_xxx methods +class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): + reset_password_token_secret = SECRET + verification_token_secret = SECRET + + async def on_after_register(self, user: User, request: Optional[Request] = None): + print(f"User {user.id} has registered.") + + async def on_after_forgot_password( + self, user: User, token: str, request: Optional[Request] = None + ): + print(f"User {user.id} has forgot their password. Reset token: {token}") + + async def on_after_request_verify( + self, user: User, token: str, request: Optional[Request] = None + ): + print(f"Verification requested for user {user.id}. Verification token: {token}") + + +async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)): + yield UserManager(user_db) + + +def get_jwt_strategy() -> JWTStrategy[models.UP, models.ID]: + return JWTStrategy(secret=SECRET, lifetime_seconds=LIFETIME) + + +bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") +cookie_transport = CookieTransport(cookie_max_age=LIFETIME) + +bearer_auth_backend = AuthenticationBackend( + name="jwt", + transport=bearer_transport, + get_strategy=get_jwt_strategy, +) +cookie_auth_backend = AuthenticationBackend( + name="cookie", + transport=cookie_transport, + get_strategy=get_jwt_strategy, +) + +fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [bearer_auth_backend, cookie_auth_backend]) + +current_active_user = fastapi_users.current_user(active=True) diff --git a/backend/src/database/config.py b/backend/src/database/config.py index 7e00808..7ae6805 100644 --- a/backend/src/database/config.py +++ b/backend/src/database/config.py @@ -2,7 +2,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict class DbConfig(BaseSettings): - model_config = SettingsConfigDict(env_prefix='DB') + model_config = SettingsConfigDict(env_prefix='DB_') HOST: str = "localhost" PORT: int = 5432 USER: str = "MediaManager" diff --git a/backend/src/database/tv.py b/backend/src/database/tv.py index 006bdf8..7d13e22 100644 --- a/backend/src/database/tv.py +++ b/backend/src/database/tv.py @@ -1,49 +1,54 @@ import uuid from uuid import UUID -from sqlalchemy import ForeignKeyConstraint, UniqueConstraint -from sqlmodel import Field, Relationship, SQLModel - -from database.torrents import TorrentMixin +from sqlalchemy import ForeignKey, ForeignKeyConstraint, Integer, String, UniqueConstraint +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship -class Show(SQLModel, table=True): +class Base(DeclarativeBase): + pass + + +class Show(Base): + __tablename__ = "show" __table_args__ = (UniqueConstraint("external_id", "metadata_provider", "version"),) - id: UUID = Field(primary_key=True, default_factory=uuid.uuid4) - external_id: int - metadata_provider: str - name: str - overview: str - # For some shows the first_air_date isn't known, therefore it needs to be nullable - year: int | None - version: str = Field(default="") - seasons: list["Season"] = Relationship(back_populates="show", cascade_delete=True) + id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid.uuid4) + external_id: Mapped[int] = mapped_column(Integer, nullable=False) + metadata_provider: Mapped[str] = mapped_column(String, nullable=False) + name: Mapped[str] = mapped_column(String, nullable=False) + overview: Mapped[str] = mapped_column(String, nullable=False) + year: Mapped[int | None] = mapped_column(Integer, nullable=True) + version: Mapped[str] = mapped_column(String, default="") + + seasons: Mapped[list["Season"]] = relationship(back_populates="show", cascade="all, delete") -class Season(SQLModel, TorrentMixin, table=True): +class Season(Base): + __tablename__ = "season" __table_args__ = (UniqueConstraint("show_id", "number"),) - id: UUID = Field(primary_key=True, default_factory=uuid.uuid4) - show_id: UUID = Field(foreign_key="show.id", ondelete="CASCADE") - number: int = Field() + id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid.uuid4) + show_id: Mapped[UUID] = mapped_column(ForeignKey("show.id", ondelete="CASCADE"), nullable=False) + number: Mapped[int] = mapped_column(Integer, nullable=False) + external_id: Mapped[int] = mapped_column(Integer, nullable=False) + name: Mapped[str] = mapped_column(String, nullable=False) + overview: Mapped[str] = mapped_column(String, nullable=False) - external_id: int - name: str - overview: str - - show: Show = Relationship(back_populates="seasons") - episodes: list["Episode"] = Relationship(back_populates="season", cascade_delete=True) + show: Mapped[Show] = relationship(back_populates="seasons") + episodes: Mapped[list["Episode"]] = relationship(back_populates="season", cascade="all, delete") -class Episode(SQLModel, table=True): +class Episode(Base): + __tablename__ = "episode" __table_args__ = ( - ForeignKeyConstraint(['show_id', 'season_number'], ['season.show_id', 'season.number'], ondelete="CASCADE"), + ForeignKeyConstraint(["show_id", "season_number"], ["season.show_id", "season.number"], ondelete="CASCADE"), ) - show_id: UUID = Field(primary_key=True) - season_number: int = Field(primary_key=True) - number: int = Field(primary_key=True) - external_id: int - title: str - season: Season = Relationship(back_populates="episodes") + show_id: Mapped[UUID] = mapped_column(ForeignKey("show.id"), primary_key=True) + season_number: Mapped[int] = mapped_column(Integer, primary_key=True) + number: Mapped[int] = mapped_column(Integer, primary_key=True) + external_id: Mapped[int] = mapped_column(Integer, nullable=False) + title: Mapped[str] = mapped_column(String, nullable=False) + + season: Mapped[Season] = relationship(back_populates="episodes") diff --git a/backend/src/database/users.py b/backend/src/database/users.py index 77f58cf..b28b04f 100644 --- a/backend/src/database/users.py +++ b/backend/src/database/users.py @@ -1,19 +1,3 @@ -import uuid -from uuid import UUID - -from sqlmodel import Field, SQLModel -class UserBase(SQLModel): - name: str = Field() - lastname: str - email: str = Field(unique=True) -class UserPublic(UserBase): - id: UUID = Field(primary_key=True, default_factory=uuid.uuid4) - -class User(UserPublic, table=True): - hashed_password: str - -class UserCreate(UserBase): - password: str \ No newline at end of file diff --git a/backend/src/dowloadClients/qbittorrent.py b/backend/src/dowloadClients/qbittorrent.py index 5ad90ee..e582ea3 100644 --- a/backend/src/dowloadClients/qbittorrent.py +++ b/backend/src/dowloadClients/qbittorrent.py @@ -14,6 +14,7 @@ class QbittorrentConfig(BaseSettings): host: str = "localhost" port: int = 8080 username: str = "admin" + password: str = "admin" class QbittorrentClient(GenericDownloadClient): diff --git a/backend/src/indexer/__init__.py b/backend/src/indexer/__init__.py index b4c4a11..d69d89e 100644 --- a/backend/src/indexer/__init__.py +++ b/backend/src/indexer/__init__.py @@ -23,5 +23,5 @@ def search(query: str | Season) -> list[IndexerQueryResult]: indexers: list[GenericIndexer] = [] -if ProwlarrConfig.enabled: +if ProwlarrConfig().enabled: indexers.append(Prowlarr()) diff --git a/backend/src/indexer/config.py b/backend/src/indexer/config.py index 783d88a..eb4e411 100644 --- a/backend/src/indexer/config.py +++ b/backend/src/indexer/config.py @@ -5,4 +5,4 @@ class ProwlarrConfig(BaseSettings): model_config = SettingsConfigDict(env_prefix="PROWLARR_") enabled: bool = True api_key: str - url: str + url: str = "http://localhost:96969" diff --git a/backend/src/indexer/prowlarr.py b/backend/src/indexer/prowlarr.py index fb4b298..badd885 100644 --- a/backend/src/indexer/prowlarr.py +++ b/backend/src/indexer/prowlarr.py @@ -17,7 +17,7 @@ class Prowlarr(GenericIndexer): :param kwargs: Additional keyword arguments to pass to the superclass constructor. """ super().__init__(name='prowlarr') - config = ProwlarrConfig + config = ProwlarrConfig() self.api_key = config.api_key self.url = config.url diff --git a/backend/src/main.py b/backend/src/main.py index 44e2111..a68a96c 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -1,7 +1,13 @@ import logging import sys +from contextlib import asynccontextmanager from logging.config import dictConfig +import database +from auth.db import create_db_and_tables +from auth.schemas import UserCreate, UserRead, UserUpdate +from auth.users import bearer_auth_backend, fastapi_users + logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(name)s - %(funcName)s(): %(message)s", stream=sys.stdout, @@ -11,10 +17,7 @@ log = logging.getLogger(__name__) import uvicorn from fastapi import FastAPI -import database.users import tv.router -from auth import password -from users import routers LOGGING_CONFIG = { "version": 1, @@ -42,11 +45,50 @@ LOGGING_CONFIG = { # Apply logging config dictConfig(LOGGING_CONFIG) + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Not needed if you setup a migration system like Alembic + await create_db_and_tables() + yield + + database.init_db() -app = FastAPI(root_path="/api/v1") -app.include_router(routers.router, tags=["users"]) -app.include_router(password.router, tags=["authentication"]) -app.include_router(tv.router.router, tags=["tv"]) +app = FastAPI(root_path="/api/v1", lifespan=lifespan) +app.include_router( + fastapi_users.get_auth_router(bearer_auth_backend), + prefix="/auth/jwt", + tags=["auth"] +) +app.include_router( + fastapi_users.get_auth_router(bearer_auth_backend), + prefix="/auth/cookie", + tags=["auth"] +) +app.include_router( + fastapi_users.get_register_router(UserRead, UserCreate), + prefix="/auth", + tags=["auth"], +) +app.include_router( + fastapi_users.get_reset_password_router(), + prefix="/auth", + tags=["auth"], +) +app.include_router( + fastapi_users.get_verify_router(UserRead), + prefix="/auth", + tags=["auth"], +) +app.include_router( + fastapi_users.get_users_router(UserRead, UserUpdate), + prefix="/users", + tags=["users"], +) + +app.include_router( + tv.router.router, + tags=["tv"]) if __name__ == "__main__": uvicorn.run(app, host="127.0.0.1", port=5049, log_config=LOGGING_CONFIG) diff --git a/backend/src/metadataProvider/tmdb.py b/backend/src/metadataProvider/tmdb.py index f3f4195..c15e245 100644 --- a/backend/src/metadataProvider/tmdb.py +++ b/backend/src/metadataProvider/tmdb.py @@ -14,7 +14,7 @@ class TmdbConfig(BaseSettings): TMDB_API_KEY: str | None = None -config = TmdbConfig +config = TmdbConfig() log = logging.getLogger(__name__) diff --git a/backend/src/requirements.txt b/backend/src/requirements.txt index 8070a72..2e70e67 100644 --- a/backend/src/requirements.txt +++ b/backend/src/requirements.txt @@ -1,7 +1,8 @@ -fastapi==0.115.11 +fastapi==0.115.12 ollama==0.4.7 psycopg==3.2.4 -pydantic==2.10.6 +pydantic==2.11.0 +pydantic_settings==2.8.1 PyJWT==2.10.1 PyJWT==2.10.1 python_bcrypt==0.3.2 diff --git a/backend/src/tv/models.py b/backend/src/tv/models.py new file mode 100644 index 0000000..1b22941 --- /dev/null +++ b/backend/src/tv/models.py @@ -0,0 +1,57 @@ +import uuid +from uuid import UUID + +from sqlalchemy import ForeignKey, ForeignKeyConstraint, Integer, String, UniqueConstraint +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship + + +class Base(DeclarativeBase): + pass + + +class Show(Base): + __tablename__ = "show" + __table_args__ = (UniqueConstraint("external_id", "metadata_provider", "version"),) + + id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid.uuid4) + external_id: Mapped[int] = mapped_column(Integer, nullable=False) + metadata_provider: Mapped[str] = mapped_column(String, nullable=False) + name: Mapped[str] = mapped_column(String, nullable=False) + overview: Mapped[str] = mapped_column(String, nullable=False) + year: Mapped[int | None] = mapped_column(Integer, nullable=True) + version: Mapped[str] = mapped_column(String, default="") + + seasons: Mapped[list["Season"]] = relationship(back_populates="show", cascade="all, delete") + + +class Season(Base): + __tablename__ = "season" + __table_args__ = (UniqueConstraint("show_id", "number"),) + + id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid.uuid4) + show_id: Mapped[UUID] = mapped_column(ForeignKey(column="show.id", ondelete="CASCADE"), nullable=False) + number: Mapped[int] = mapped_column(Integer, nullable=False) + external_id: Mapped[int] = mapped_column(Integer, nullable=False) + name: Mapped[str] = mapped_column(String, nullable=False) + overview: Mapped[str] = mapped_column(String, nullable=False) + torrent_id: Mapped[UUID] = mapped_column(ForeignKey(column="torrent.id"), nullable=False) + + show: Mapped[Show] = relationship(back_populates="seasons") + episodes: Mapped[list["Episode"]] = relationship(back_populates="season", cascade="all, delete") + + +class Episode(Base): + __tablename__ = "episode" + __table_args__ = ( + ForeignKeyConstraint(columns=["show_id", "season_number"], + refcolumns=["season.show_id", "season.number"], + ondelete="CASCADE"), + ) + + show_id: Mapped[UUID] = mapped_column(ForeignKey("show.id"), primary_key=True) + season_number: Mapped[int] = mapped_column(Integer, primary_key=True) + number: Mapped[int] = mapped_column(Integer, primary_key=True) + external_id: Mapped[int] = mapped_column(Integer, nullable=False) + title: Mapped[str] = mapped_column(String, nullable=False) + + season: Mapped[Season] = relationship(back_populates="episodes") diff --git a/backend/src/tv/router.py b/backend/src/tv/router.py index dbd4396..d684568 100644 --- a/backend/src/tv/router.py +++ b/backend/src/tv/router.py @@ -8,16 +8,15 @@ from fastapi.responses import JSONResponse from pydantic import BaseModel from sqlmodel import select -import auth import dowloadClients import indexer import metadataProvider +from auth.users import current_active_user from database import DbSessionDependency from database.torrents import Torrent from database.tv import Season, Show from indexer import IndexerQueryResult from tv import log -from users.routers import Message router = APIRouter( prefix="/tv", @@ -29,10 +28,10 @@ class ShowDetails(BaseModel): seasons: list[Season] -@router.post("/show", status_code=status.HTTP_201_CREATED, dependencies=[Depends(auth.get_current_user)], +@router.post("/show", status_code=status.HTTP_201_CREATED, dependencies=[Depends(current_active_user)], responses={ status.HTTP_201_CREATED: {"model": Show, "description": "Successfully created show"}, - status.HTTP_409_CONFLICT: {"model": Message, "description": "Show already exists"}, + status.HTTP_409_CONFLICT: {"model": str, "description": "Show already exists"}, }) def add_show(db: DbSessionDependency, show_id: int, metadata_provider: str = "tmdb", version: str = ""): res = db.exec(select(Show). @@ -58,13 +57,13 @@ def add_show(db: DbSessionDependency, show_id: int, metadata_provider: str = "tm return show -@router.delete("/{show_id}", status_code=status.HTTP_200_OK) +@router.delete("/{show_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)]) def delete_show(db: DbSessionDependency, show_id: UUID): db.delete(db.get(Show, show_id)) db.commit() -@router.patch("/{show_id}/{season_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(auth.get_current_user)], +@router.patch("/{show_id}/{season_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)], response_model=Season) def add_season(db: DbSessionDependency, season_id: UUID): """ @@ -79,7 +78,7 @@ def add_season(db: DbSessionDependency, season_id: UUID): return season -@router.delete("/{show_id}/{season_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(auth.get_current_user)], +@router.delete("/{show_id}/{season_id}", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)], response_model=Show) def delete_season(db: DbSessionDependency, show_id: UUID, season: int): """ @@ -94,7 +93,7 @@ def delete_season(db: DbSessionDependency, show_id: UUID, season: int): @router.get("/{show_id}/{season_id}/torrent", status_code=status.HTTP_200_OK, dependencies=[Depends( - auth.get_current_user)], + current_active_user)], response_model=list[IndexerQueryResult]) def get_season_torrents(db: DbSessionDependency, show_id: UUID, season_id: UUID): season = db.get(Season, season_id) @@ -120,7 +119,7 @@ def get_season_torrents(db: DbSessionDependency, show_id: UUID, season_id: UUID) @router.post("/{show_id}/torrent", status_code=status.HTTP_200_OK, dependencies=[Depends( - auth.get_current_user)], response_model=list[Season]) + current_active_user)], response_model=list[Season]) def download_seasons_torrent(db: DbSessionDependency, show_id: UUID, torrent_id: UUID): """ downloads torrents for a show season, links the torrent for all seasons the torrent contains @@ -152,7 +151,7 @@ def download_seasons_torrent(db: DbSessionDependency, show_id: UUID, torrent_id: @router.post("/{show_id}/{season_id}/torrent", status_code=status.HTTP_200_OK, dependencies=[Depends( - auth.get_current_user)], response_model=list[Season]) + current_active_user)], response_model=list[Season]) def delete_seasons_torrent(db: DbSessionDependency, show_id: UUID, season_id: UUID, torrent_id: UUID): """ downloads torrents for a season, links the torrent only to the specified season @@ -185,13 +184,13 @@ def delete_seasons_torrent(db: DbSessionDependency, show_id: UUID, season_id: UU return seasons -@router.get("/", dependencies=[Depends(auth.get_current_user)], response_model=list[Show]) +@router.get("/", dependencies=[Depends(current_active_user)], response_model=list[Show]) def get_shows(db: DbSessionDependency): """""" return db.exec(select(Show)).unique().fetchall() -@router.get("/{show_id}", dependencies=[Depends(auth.get_current_user)], response_model=ShowDetails) +@router.get("/{show_id}", dependencies=[Depends(current_active_user)], response_model=ShowDetails) def get_show(db: DbSessionDependency, show_id: UUID): """ @@ -210,6 +209,6 @@ def get_show(db: DbSessionDependency, show_id: UUID): return ShowDetails(show=shows.first()[0], seasons=seasons) -@router.get("/search") +@router.get("/search", dependencies=[Depends(current_active_user)]) def search_show(query: str, metadata_provider: str = "tmdb"): return metadataProvider.search_show(query, metadata_provider) diff --git a/backend/src/users/__init__.py b/backend/src/users/__init__.py deleted file mode 100644 index 3988bf1..0000000 --- a/backend/src/users/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import logging - -log = logging.getLogger(__name__) diff --git a/backend/src/users/routers.py b/backend/src/users/routers.py deleted file mode 100644 index 8046f3b..0000000 --- a/backend/src/users/routers.py +++ /dev/null @@ -1,54 +0,0 @@ -from fastapi import APIRouter, Depends -from pydantic import BaseModel -from sqlalchemy.exc import IntegrityError -from starlette.responses import JSONResponse - -from auth import get_current_user -from auth.password import get_password_hash -from database import DbSessionDependency -from database.users import User, UserCreate, UserPublic -from users import log - -router = APIRouter( - prefix="/users", -) - -# TODO: remove from users.py -class Message(BaseModel): - message: str - - - -@router.post("/", status_code=201, responses={ - 409: {"model": Message, "description": "User with provided email already exists"}, - 201: {"model": UserPublic, "description": "User created successfully"} -}) -async def create_user( - db: DbSessionDependency, - user: UserCreate = Depends(UserCreate), -): - internal_user = User(name=user.name, lastname=user.lastname, email=user.email, - hashed_password=get_password_hash(user.password)) - db.add(internal_user) - try: - db.commit() - except IntegrityError as e: - log.debug(e) - log.warning("Failed to create new USER, User with this email already exists " + internal_user.model_dump().__str__()) - return JSONResponse(status_code=409, content={"message": "User with this email already exists"}) - log.info("Created new USER " + internal_user.email) - return UserPublic(**internal_user.model_dump()) - - -@router.get("/me") -async def read_users_me( - current_user: User = Depends(get_current_user), -): - return JSONResponse(status_code=200, content=current_user.model_dump()) - - -@router.get("/me/items") -async def read_own_items( - current_user: User = Depends(get_current_user), -): - return [{"item_id": "Foo", "owner": current_user.name}] diff --git a/docker-compose.yml b/docker-compose.yml index 068a827..0f6a5b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: image: postgres:latest restart: unless-stopped volumes: - - .\MediaManager\res\postgres:/var/lib/postgresql/data + - .\backend\res\postgres:/var/lib/postgresql/data environment: POSTGRES_USER: MediaManager POSTGRES_DB: MediaManager @@ -18,12 +18,26 @@ services: - PGID=1000 - TZ=Etc/UTC volumes: - - .\MediaManager\res\prowlarr:/config + - .\backend\res\prowlarr:/config ports: - "9696:9696" ollama: image: ollama/ollama volumes: - - .\MediaManager\res\ollama:/root/.ollama + - .\backend\res\ollama:/root/.ollama ports: - - "11434:11434" \ No newline at end of file + - "11434:11434" + qbittorrent: + image: lscr.io/linuxserver/qbittorrent:latest + container_name: qbittorrent + environment: + - PUID=1000 + - PGID=1000 + - TZ=Etc/UTC + - WEBUI_PORT=8080 + - TORRENTING_PORT=6881 + ports: + - 8080:8080 + - 6881:6881 + - 6881:6881/udp + restart: unless-stopped \ No newline at end of file