This commit is contained in:
maxDorninger
2025-03-28 19:21:11 +01:00
parent f13228dba9
commit 3a80f874f9
22 changed files with 299 additions and 271 deletions

View File

@@ -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

View File

@@ -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

34
backend/src/auth/db.py Normal file
View File

@@ -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)

View File

@@ -1,4 +0,0 @@
#TODO: Implement OAuth2/Open ID Connect

View File

@@ -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")

View File

@@ -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

64
backend/src/auth/users.py Normal file
View File

@@ -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)

View File

@@ -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"

View File

@@ -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")

View File

@@ -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

View File

@@ -14,6 +14,7 @@ class QbittorrentConfig(BaseSettings):
host: str = "localhost"
port: int = 8080
username: str = "admin"
password: str = "admin"
class QbittorrentClient(GenericDownloadClient):

View File

@@ -23,5 +23,5 @@ def search(query: str | Season) -> list[IndexerQueryResult]:
indexers: list[GenericIndexer] = []
if ProwlarrConfig.enabled:
if ProwlarrConfig().enabled:
indexers.append(Prowlarr())

View File

@@ -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"

View File

@@ -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

View File

@@ -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)

View File

@@ -14,7 +14,7 @@ class TmdbConfig(BaseSettings):
TMDB_API_KEY: str | None = None
config = TmdbConfig
config = TmdbConfig()
log = logging.getLogger(__name__)

View File

@@ -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

57
backend/src/tv/models.py Normal file
View File

@@ -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")

View File

@@ -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)

View File

@@ -1,3 +0,0 @@
import logging
log = logging.getLogger(__name__)

View File

@@ -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}]