secure media endpoints
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2s

This commit is contained in:
2026-05-17 21:48:40 +02:00
parent cd033f458d
commit 6dd8e12218
4 changed files with 138 additions and 51 deletions
+75 -25
View File
@@ -1,7 +1,14 @@
from typing import List
from fastapi import APIRouter, status, HTTPException, Depends from fastapi import APIRouter, status, HTTPException, Depends
from sqlalchemy import select, Sequence from sqlalchemy import select, Sequence
from src.core.log_conf import logger from src.core.log_conf import logger
from src.db.repository.media import create_new_mediaactorfile, create_new_mediafile, delete_mediafile from src.core.security import UserDep, get_current_user_from_token
from src.db.repository.media import (
create_new_mediaactorfile,
create_new_mediafile,
delete_mediafile,
)
from src.db.session import SessionDep from src.db.session import SessionDep
from src.schema.media.actor import MediaActorResponse from src.schema.media.actor import MediaActorResponse
from src.schema.media.actorfile import MediaActorFileResponse from src.schema.media.actorfile import MediaActorFileResponse
@@ -10,10 +17,14 @@ from src.db.models.media import MediaFile
router = APIRouter() router = APIRouter()
@router.get("/update-titles") @router.get("/update-titles")
def update_titles(db: SessionDep) -> list[MediaFileResponse]: # type: ignore def update_titles(db: SessionDep) -> list[MediaFileResponse]:
"""
Update title for given MediaFile.
"""
results: list[MediaFileResponse] = [] results: list[MediaFileResponse] = []
files = db.query(MediaFile).filter(MediaFile.review == True).all() files = db.query(MediaFile).filter(MediaFile.review.is_(True)).all()
for mediafile in files: for mediafile in files:
mediafile.update_title() mediafile.update_title()
db.add(mediafile) db.add(mediafile)
@@ -23,61 +34,92 @@ def update_titles(db: SessionDep) -> list[MediaFileResponse]: # type: ignore
return results return results
@router.get("/files", response_model=list[MediaFileResponse]) @router.get(
# def get_all_files(db: SessionDep, review: bool = False, download: bool = False, current_user: Profile = Depends(get_current_user_from_token)) -> List[MediaFileResponse]: "/files",
def get_all_files(db: SessionDep, review: bool = False, download: bool = False) -> list[MediaFileResponse]: # type: ignore response_model=list[MediaFileResponse],
results: list[MediaFileResponse] = [] dependencies=[Depends(get_current_user_from_token)],
files: Sequence[MediaFile] )
def get_all_files(
db: SessionDep, review: bool = False, download: bool = False
) -> List[MediaFileResponse]:
"""
Get all MediaFiles.
"""
results: List[MediaFileResponse] = []
files: List[MediaFile]
if review: if review:
files = db.query(MediaFile).filter(MediaFile.review == True).all() # type: ignore files = db.query(MediaFile).filter(MediaFile.review.is_(True)).all()
elif download: elif download:
files = db.query(MediaFile).filter(MediaFile.should_download == True).all() # type: ignore files = db.query(MediaFile).filter(MediaFile.should_download.is_(True)).all()
else: else:
files = db.scalars(select(MediaFile)).all() # type: ignore files = db.query(MediaFile).all()
for mediafile in files: # type: ignore for mediafile in files:
response = get_file_details(mediafile) response = get_file_details(mediafile)
results.append(response) results.append(response)
return results return results
@router.get("/files/{file_id}", response_model=MediaFileResponse)
def get_file(file_id: str, db: SessionDep) -> MediaFileResponse: # type: ignore @router.get(
"/files/{file_id}",
response_model=MediaFileResponse,
dependencies=[Depends(get_current_user_from_token)],
)
def get_file(file_id: str, db: SessionDep) -> MediaFileResponse:
"""
Get MediaFile with given id or return HTTPException.
"""
mediafile = db.get(MediaFile, file_id) mediafile = db.get(MediaFile, file_id)
if not mediafile: if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found") raise HTTPException(status_code=404, detail="MediaFile could not be found")
response = get_file_details(mediafile) response = get_file_details(mediafile)
return response return response
@router.delete("/files/{file_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/files/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_file(file_id: str, db: SessionDep): # type: ignore def delete_file(file_id: str, db: SessionDep):
"""
Delete MediaFile by given id.
"""
mediafile = db.get(MediaFile, file_id) mediafile = db.get(MediaFile, file_id)
if not mediafile: if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found") raise HTTPException(status_code=404, detail="MediaFile could not be found")
logger.info(f"delete MediaFile: {file_id}") logger.info("delete MediaFile: %s", file_id)
actor_files = mediafile.media_actor_files actor_files = mediafile.media_actor_files
logger.info(f"MediaActorFiles links {len(actor_files)}") logger.info("MediaActorFiles links %s", len(actor_files))
if len(actor_files) == 0: if len(actor_files) == 0:
delete_mediafile(db, mediafile.id) delete_mediafile(db, mediafile.id)
@router.get("/files/{file_id}/actors", response_model=list[MediaActorResponse]) @router.get("/files/{file_id}/actors", response_model=list[MediaActorResponse])
def get_file_actors(file_id: str, db: SessionDep) -> list[MediaActorResponse]: # type: ignore def get_file_actors(file_id: str, db: SessionDep) -> list[MediaActorResponse]:
"""
Get list of ACtors for given MediaFile.
"""
mediafile = db.get(MediaFile, file_id) mediafile = db.get(MediaFile, file_id)
if not mediafile: if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found") raise HTTPException(status_code=404, detail="MediaFile could not be found")
actor_files = mediafile.media_actor_files actor_files = mediafile.media_actor_files
logger.info(f"already known actors: {actor_files}") logger.info("already known actors: %s", actor_files)
results: list[MediaActorResponse] = [] results: list[MediaActorResponse] = []
for actor_file in actor_files: for actor_file in actor_files:
response = MediaActorResponse(id=actor_file.media_actor.id, name=actor_file.media_actor.name, url=actor_file.media_actor.url) response = MediaActorResponse(
id=actor_file.media_actor.id,
name=actor_file.media_actor.name,
url=str(actor_file.media_actor.url),
)
results.append(response) results.append(response)
return results return results
@router.put("/files/{file_id}/actors", response_model=list[MediaActorFileResponse]) @router.put("/files/{file_id}/actors", response_model=list[MediaActorFileResponse])
def update_file_actors(file_id: str, db: SessionDep, actors: list[MediaActorResponse]) -> list[MediaActorFileResponse]: # type: ignore def update_file_actors(
file_id: str, db: SessionDep, actors: list[MediaActorResponse]
) -> list[MediaActorFileResponse]: # type: ignore
mediafile = db.get(MediaFile, file_id) mediafile = db.get(MediaFile, file_id)
if not mediafile: if not mediafile:
raise HTTPException(status_code=404, detail="MediaFile could not be found") raise HTTPException(status_code=404, detail="MediaFile could not be found")
actor_files = mediafile.media_actor_files actor_files = mediafile.media_actor_files
logger.info(f"already known actors: {actor_files}") logger.info("already known actors: %s", actor_files)
for actor in actors: for actor in actors:
already_associated = False already_associated = False
for actor_file in actor_files: for actor_file in actor_files:
@@ -91,12 +133,19 @@ def update_file_actors(file_id: str, db: SessionDep, actors: list[MediaActorResp
actor_files = mediafile.media_actor_files actor_files = mediafile.media_actor_files
results: list[MediaActorFileResponse] = [] results: list[MediaActorFileResponse] = []
for actor_file in actor_files: for actor_file in actor_files:
response = MediaActorFileResponse(id=actor_file.id, actor_id=actor_file.media_actor_id, file_id=actor_file.media_file_id) response = MediaActorFileResponse(
id=actor_file.id,
actor_id=actor_file.media_actor_id,
file_id=actor_file.media_file_id,
)
results.append(response) results.append(response)
return results return results
@router.put("/files/{file_id}", response_model=MediaFileResponse) @router.put("/files/{file_id}", response_model=MediaFileResponse)
def update_file(file_id: str, db: SessionDep, info: MediaFileResponse) -> MediaFileResponse: # type: ignore def update_file(
file_id: str, db: SessionDep, info: MediaFileResponse
) -> MediaFileResponse: # type: ignore
mediaFile = db.get(MediaFile, file_id) mediaFile = db.get(MediaFile, file_id)
if not mediaFile: if not mediaFile:
raise HTTPException(status_code=404, detail="MediaFile could not be found") raise HTTPException(status_code=404, detail="MediaFile could not be found")
@@ -109,8 +158,9 @@ def update_file(file_id: str, db: SessionDep, info: MediaFileResponse) -> MediaF
response = get_file_details(mediafile) response = get_file_details(mediafile)
return response return response
@router.post("/files", status_code=status.HTTP_201_CREATED) @router.post("/files", status_code=status.HTTP_201_CREATED)
def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse: # type: ignore def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse: # type: ignore
logger.info(f"add url {new_link.url}") logger.info(f"add url {new_link.url}")
try: try:
mediaFile: MediaFile = create_new_mediafile(new_link.url, db) mediaFile: MediaFile = create_new_mediafile(new_link.url, db)
+19 -4
View File
@@ -13,13 +13,23 @@ from pydantic import ValidationError
from src.core.config import settings from src.core.config import settings
from src.core.log_conf import logger from src.core.log_conf import logger
from src.db.models.admin import Profile from src.db.models.admin import Profile
from src.db.repository.admin import get_profile_by_username, get_profile_by_email, is_database_empty from src.db.repository.admin import (
get_profile_by_username,
get_profile_by_email,
is_database_empty,
)
from src.db.session import SessionLocal from src.db.session import SessionLocal
from src.schema.admin import ProfileModel, TokenData from src.schema.admin import ProfileModel, TokenData
oauth2_scheme = OAuth2PasswordBearer( oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="/token", tokenUrl="/token",
scopes={"me": "read", "admin": "read", "ROLE_ADMIN": "admin", "ROLE_MEDIA": "media", "ROLE_USER": "user"}, scopes={
"me": "read",
"admin": "read",
"ROLE_ADMIN": "admin",
"ROLE_MEDIA": "media",
"ROLE_USER": "user",
},
) )
@@ -51,6 +61,7 @@ oauth2_scheme = OAuth2PasswordBearer(
# return None # return None
# return param # return param
def authenticate_user_by_email(email: str, password: str) -> Optional[Profile]: def authenticate_user_by_email(email: str, password: str) -> Optional[Profile]:
with SessionLocal() as db: with SessionLocal() as db:
user = get_profile_by_email(email=email, db=db) user = get_profile_by_email(email=email, db=db)
@@ -161,6 +172,7 @@ async def get_current_active_user(
def get_current_user_from_token(token: str = Depends(oauth2_scheme)): def get_current_user_from_token(token: str = Depends(oauth2_scheme)):
""" """
credentials_exception = HTTPException( credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials", detail="Could not validate credentials",
@@ -173,10 +185,13 @@ def get_current_user_from_token(token: str = Depends(oauth2_scheme)):
logger.info("username/email extracted is %s", username) logger.info("username/email extracted is %s", username)
if username is None: if username is None:
raise credentials_exception raise credentials_exception
except JWTError: except JWTError as exception:
raise credentials_exception raise credentials_exception from exception
with SessionLocal() as db: with SessionLocal() as db:
user = get_profile_by_email(email=username, db=db) user = get_profile_by_email(email=username, db=db)
if user is None: if user is None:
raise credentials_exception raise credentials_exception
return user return user
UserDep = Annotated[Profile, Depends(get_current_user_from_token)]
+37 -17
View File
@@ -14,51 +14,71 @@ from src.db.models.base import Base, BaseMixin, BaseVideoMixin
class MediaFile(Base, BaseMixin, BaseVideoMixin): class MediaFile(Base, BaseMixin, BaseVideoMixin):
__tablename__ = 'media_file' """
media_actor_files: Mapped[List["MediaActorFile"]] = relationship(back_populates="media_file") MediaFile represents video link.
"""
__tablename__ = "media_file"
media_actor_files: Mapped[List["MediaActorFile"]] = relationship(
back_populates="media_file"
)
def __repr__(self): def __repr__(self):
return f'MediaFile({self.id} {self.title} {self.title})' return f"MediaFile({self.id} {self.title} {self.title})"
def __str__(self): def __str__(self):
return f'{self.title}({self.id})' return f"{self.title}({self.id})"
def update_title(self):
"""
Update title from url.
"""
class MediaActor(Base, BaseMixin): class MediaActor(Base, BaseMixin):
__tablename__ = 'media_actor' __tablename__ = "media_actor"
name: Mapped[str] name: Mapped[str]
url: Mapped[Optional[str]] = mapped_column(unique=True) url: Mapped[Optional[str]] = mapped_column(unique=True)
media_actor_files = relationship("MediaActorFile") media_actor_files = relationship("MediaActorFile")
def __repr__(self) -> str: def __repr__(self) -> str:
return f'MediaActor({self.id} {self.name} {self.url})' return f"MediaActor({self.id} {self.name} {self.url})"
def __str__(self) -> str: def __str__(self) -> str:
return f'{self.url}({self.id})' return f"{self.url}({self.id})"
class MediaActorFile(Base, BaseMixin): class MediaActorFile(Base, BaseMixin):
__tablename__ = 'media_actor_file' __tablename__ = "media_actor_file"
media_actor_id: Mapped[str] = mapped_column(ForeignKey("media_actor.id"), nullable=False) media_actor_id: Mapped[str] = mapped_column(
ForeignKey("media_actor.id"), nullable=False
)
media_actor: Mapped[MediaActor] = relationship(back_populates="media_actor_files") media_actor: Mapped[MediaActor] = relationship(back_populates="media_actor_files")
media_file_id: Mapped[str] = mapped_column(ForeignKey("media_file.id"), nullable=True) media_file_id: Mapped[str] = mapped_column(
ForeignKey("media_file.id"), nullable=True
)
media_file: Mapped[MediaFile] = relationship(back_populates="media_actor_files") media_file: Mapped[MediaFile] = relationship(back_populates="media_actor_files")
def __repr__(self): def __repr__(self):
return f'MediaActorFile({self.id} {self.media_actor_id} {self.media_file_id})' return f"MediaActorFile({self.id} {self.media_actor_id} {self.media_file_id})"
def __str__(self) -> str: def __str__(self) -> str:
return f'{self.id} {self.media_actor_id} {self.media_file_id}' return f"{self.id} {self.media_actor_id} {self.media_file_id}"
class MediaArticle(Base, BaseMixin): class MediaArticle(Base, BaseMixin):
__tablename__ = 'media_article' __tablename__ = "media_article"
review: Mapped[bool] review: Mapped[bool]
title: Mapped[str] title: Mapped[str]
url: Mapped[str] = mapped_column(unique=True) url: Mapped[str] = mapped_column(unique=True)
class MediaVideo(Base, BaseMixin): class MediaVideo(Base, BaseMixin):
__tablename__ = 'media_video' """
MediaFile represents video link.
"""
__tablename__ = "media_video"
cloud_link: Mapped[str] cloud_link: Mapped[str]
file_name: Mapped[str] file_name: Mapped[str]
path: Mapped[str] path: Mapped[str]
@@ -68,10 +88,10 @@ class MediaVideo(Base, BaseMixin):
should_download: Mapped[bool] should_download: Mapped[bool]
def __repr__(self): def __repr__(self):
return f'MediaFile({self.id} {self.title} {self.url})' return f"MediaFile({self.id} {self.title} {self.url})"
def __str__(self): def __str__(self):
if self.title is None: if self.title is None:
return f'{self.url}({self.id})' return f"{self.url}({self.id})"
else: else:
return f'{self.title}({self.id})' return f"{self.title}({self.id})"
+4 -2
View File
@@ -12,8 +12,10 @@ engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(bind=engine) SessionLocal = sessionmaker(bind=engine)
def get_db() -> Generator: def get_db() -> Generator[Session, None, None]:
""" """
with SessionLocal() as db: with SessionLocal() as db:
yield db yield db
SessionDep: type[Session] = Annotated[Session, Depends(get_db)]
SessionDep = Annotated[Session, Depends(get_db)]