diff --git a/kontor-api/src/apis/base.py b/kontor-api/src/apis/base.py index 10dd55e..f1ad14a 100644 --- a/kontor-api/src/apis/base.py +++ b/kontor-api/src/apis/base.py @@ -3,20 +3,25 @@ add router for different parts (like comics, tysc, media) """ from fastapi import APIRouter, Depends -from src.apis.version1.admin import token -from src.apis.version1.comics import artist, comic, issue -from src.apis.version1.media import mediaactor, mediaactorfile, mediafile +from src.apis.version1.admin import mailaccount +from src.apis.version1.comics import artist, comic, issue, worktype, volume, storyarc +from src.apis.version1.media import mediaactor, mediaactorfile, mediafile, mediavideo, mediaarticle from src.apis.version1.tysc import card, cardset, fieldposition, player, rooster, sport, team, vendor from src.core.security import get_current_user_from_token -from src.apis.version1.user import profile +from src.apis.version1.user import assignment, permission, profile, token from src.apis.version1.bookshelf import article api_router = APIRouter(prefix="/api") api_router.include_router(comic.router, prefix="/comics", tags=["comics"], dependencies=[Depends(get_current_user_from_token)]) api_router.include_router(artist.router, prefix="/comics", tags=["comics"], dependencies=[Depends(get_current_user_from_token)]) api_router.include_router(issue.router, prefix="/comics", tags=["comics"], dependencies=[Depends(get_current_user_from_token)]) +api_router.include_router(worktype.router, prefix="/comics", tags=["comics"], dependencies=[Depends(get_current_user_from_token)]) +api_router.include_router(volume.router, prefix="/comics", tags=["comics"], dependencies=[Depends(get_current_user_from_token)]) +api_router.include_router(storyarc.router, prefix="/comics", tags=["comics"], dependencies=[Depends(get_current_user_from_token)]) api_router.include_router(mediafile.router, prefix="/media", tags=["media"], dependencies=[Depends(get_current_user_from_token)]) +api_router.include_router(mediavideo.router, prefix="/media", tags=["media"], dependencies=[Depends(get_current_user_from_token)]) +api_router.include_router(mediaarticle.router, prefix="/media", tags=["media"], dependencies=[Depends(get_current_user_from_token)]) api_router.include_router(mediaactor.router, prefix="/media", tags=["media"], dependencies=[Depends(get_current_user_from_token)]) api_router.include_router(mediaactorfile.router, prefix="/media", tags=["media"], dependencies=[Depends(get_current_user_from_token)]) api_router.include_router(sport.router, prefix="/tysc", tags=["tysc"], dependencies=[Depends(get_current_user_from_token)]) @@ -28,5 +33,8 @@ api_router.include_router(vendor.router, prefix="/tysc", tags=["tysc"], dependen api_router.include_router(cardset.router, prefix="/tysc", tags=["tysc"], dependencies=[Depends(get_current_user_from_token)]) api_router.include_router(card.router, prefix="/tysc", tags=["tysc"], dependencies=[Depends(get_current_user_from_token)]) api_router.include_router(article.router, prefix="/bookshelf", tags=["bookshelf"], dependencies=[Depends(get_current_user_from_token)]) -api_router.include_router(token.router, prefix="/login", tags=["login"]) api_router.include_router(profile.router, prefix="/user", tags=["user"], dependencies=[Depends(get_current_user_from_token)]) +api_router.include_router(token.router, prefix="/user", tags=["user"], dependencies=[Depends(get_current_user_from_token)]) +api_router.include_router(permission.router, prefix="/user", tags=["user"], dependencies=[Depends(get_current_user_from_token)]) +api_router.include_router(assignment.router, prefix="/user", tags=["user"], dependencies=[Depends(get_current_user_from_token)]) +api_router.include_router(mailaccount.router, prefix="/admin", tags=["admin"], dependencies=[Depends(get_current_user_from_token)]) diff --git a/kontor-api/src/apis/version1/admin/login.py b/kontor-api/src/apis/version1/admin/login.py index 1545202..344dff9 100644 --- a/kontor-api/src/apis/version1/admin/login.py +++ b/kontor-api/src/apis/version1/admin/login.py @@ -1,8 +1,7 @@ from datetime import timedelta -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, Response, status from fastapi.security import OAuth2PasswordRequestForm -from pydantic import BaseModel from typing import Annotated from src.core.config import settings from src.core.log_conf import logger @@ -11,16 +10,12 @@ from src.core.security import ( authenticate_user_by_username, create_access_token, ) -from src.schema.admin import Token +from src.schema.admin.login import LoginRequest +from src.schema.admin.token import Token +from src.webapps.auth.forms import LoginForm login_router = APIRouter() - -class LoginRequest(BaseModel): - email: str | None = None - password: str | None = None - - @login_router.post( "/login", tags=["login"], @@ -47,9 +42,8 @@ def login(request: LoginRequest) -> Token: @login_router.post("/token", tags=["login"], summary="Login for access token") -async def login_for_access_token( - form_data: Annotated[OAuth2PasswordRequestForm, Depends()], -) -> Token: +#async def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token: +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()) -> Token: user = authenticate_user_by_username(form_data.username, form_data.password) if not user: raise HTTPException(status_code=400, detail="Incorrect username or password") @@ -59,3 +53,19 @@ async def login_for_access_token( expires_delta=access_token_expires, ) return Token(access_token=access_token, token_type="bearer") + +def login_for_token_cookie(response: Response, form_data: LoginForm = Depends()): + user = authenticate_user_by_email(str(form_data.username), str(form_data.password)) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + ) + access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.email}, expires_delta=access_token_expires + ) + response.set_cookie( + key="access_token", value=f"Bearer {access_token}", httponly=True + ) + return {"access_token": access_token, "token_type": "bearer"} diff --git a/kontor-api/src/apis/version1/admin/mailaccount.py b/kontor-api/src/apis/version1/admin/mailaccount.py index 8bab460..191346b 100644 --- a/kontor-api/src/apis/version1/admin/mailaccount.py +++ b/kontor-api/src/apis/version1/admin/mailaccount.py @@ -4,7 +4,7 @@ from fastapi import APIRouter from src.db.models.admin import MailAccount from src.db.session import SessionDep -from src.schema.admin import MailAccountResponse, to_response +from src.schema.admin.mailaccount import MailAccountResponse, to_response router = APIRouter() diff --git a/kontor-api/src/apis/version1/admin/token.py b/kontor-api/src/apis/version1/admin/token.py deleted file mode 100644 index 1ceeaea..0000000 --- a/kontor-api/src/apis/version1/admin/token.py +++ /dev/null @@ -1,54 +0,0 @@ -from datetime import timedelta -from typing import Annotated - -from fastapi import APIRouter, Body, HTTPException, status, Depends, Response -from fastapi.security import OAuth2PasswordRequestForm - -from src.core.config import settings -from src.core.log_conf import logger -from src.core.security import create_access_token, authenticate_user_by_email, authenticate_user_by_username, get_current_active_user -from src.db.models.admin import Profile -from src.schema.admin import Token, ProfileModel -from src.webapps.auth.forms import LoginForm - -router = APIRouter() - - -@router.post("/token") -def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token: - user = authenticate_user_by_username(form_data.username, form_data.password) - logger.info(f"Request /token: login with {form_data.username}") - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Bearer"}, - ) - access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) - access_token = create_access_token( - data={"sub": user.email, "scope": " ".join(form_data.scopes)}, expires_delta=access_token_expires - ) - return Token(access_token=access_token, token_type="bearer") - - -# @router.post("/token-cookie", response_model=Token) -def login_for_token_cookie(response: Response, form_data: LoginForm = Depends()): - user = authenticate_user_by_email(form_data.username, form_data.password) # type: ignore - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - ) - access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) - access_token = create_access_token( - data={"sub": user.email}, expires_delta=access_token_expires - ) - response.set_cookie( - key="access_token", value=f"Bearer {access_token}", httponly=True - ) - return {"access_token": access_token, "token_type": "bearer"} - - -@router.get("/profile", response_model=ProfileModel) -async def read_profile(current_user: Annotated[Profile, Depends(get_current_active_user)]): - return current_user diff --git a/kontor-api/src/apis/version1/comics/issue.py b/kontor-api/src/apis/version1/comics/issue.py index 6b180f6..9aa97d3 100644 --- a/kontor-api/src/apis/version1/comics/issue.py +++ b/kontor-api/src/apis/version1/comics/issue.py @@ -3,17 +3,17 @@ from typing import List from fastapi import APIRouter from src.db.models.comic import Issue -from src.db.repository.comics.comic import get_issue_details from src.db.session import SessionDep -from src.schema.comics.issue_details import IssueDetailsResponse +from src.schema.comics.issue import IssueResponse, to_response router = APIRouter() -@router.get("/issues", response_model=List[IssueDetailsResponse]) -def get_issues(db: SessionDep) -> List[IssueDetailsResponse]: - results: List[IssueDetailsResponse] = [] +@router.get("/issues", response_model=List[IssueResponse]) +def get_issues(db: SessionDep) -> List[IssueResponse]: + results: List[IssueResponse] = [] issues = db.query(Issue).all() for issue in issues: - results.append(get_issue_details(issue)) + response = to_response(issue) + results.append(response) return results diff --git a/kontor-api/src/apis/version1/comics/storyarc.py b/kontor-api/src/apis/version1/comics/storyarc.py new file mode 100644 index 0000000..c5af364 --- /dev/null +++ b/kontor-api/src/apis/version1/comics/storyarc.py @@ -0,0 +1,18 @@ +from typing import List + +from fastapi import APIRouter + +from src.db.models.comic import StoryArc +from src.db.session import SessionDep +from src.schema.comics.storyarc import StoryArcResponse, to_response + +router = APIRouter() + +@router.get("/storyarcs", response_model=List[StoryArcResponse]) +def get_issues(db: SessionDep) -> List[StoryArcResponse]: + results: List[StoryArcResponse] = [] + storyarcs = db.query(StoryArc).all() + for storyarc in storyarcs: + response = to_response(storyarc) + results.append(response) + return results diff --git a/kontor-api/src/apis/version1/comics/volume.py b/kontor-api/src/apis/version1/comics/volume.py new file mode 100644 index 0000000..99f50ac --- /dev/null +++ b/kontor-api/src/apis/version1/comics/volume.py @@ -0,0 +1,18 @@ +from typing import List + +from fastapi import APIRouter + +from src.db.models.comic import Volume +from src.db.session import SessionDep +from src.schema.comics.volume import VolumeResponse, to_response + +router = APIRouter() + +@router.get("/volumes", response_model=List[VolumeResponse]) +def get_issues(db: SessionDep) -> List[VolumeResponse]: + results: List[VolumeResponse] = [] + worktypes = db.query(Volume).all() + for worktype in worktypes: + response = to_response(worktype) + results.append(response) + return results diff --git a/kontor-api/src/apis/version1/comics/worktype.py b/kontor-api/src/apis/version1/comics/worktype.py new file mode 100644 index 0000000..357c5f0 --- /dev/null +++ b/kontor-api/src/apis/version1/comics/worktype.py @@ -0,0 +1,19 @@ +from typing import List + +from fastapi import APIRouter + +from src.db.models.comic import WorkType +from src.db.session import SessionDep +from src.schema.comics.worktype import WorktypeResponse, to_response + + +router = APIRouter() + +@router.get("/worktypes", response_model=List[WorktypeResponse]) +def get_issues(db: SessionDep) -> List[WorktypeResponse]: + results: List[WorktypeResponse] = [] + worktypes = db.query(WorkType).all() + for worktype in worktypes: + response = to_response(worktype) + results.append(response) + return results diff --git a/kontor-api/src/apis/version1/healthcheck.py b/kontor-api/src/apis/version1/healthcheck.py index c531634..33d1e7e 100644 --- a/kontor-api/src/apis/version1/healthcheck.py +++ b/kontor-api/src/apis/version1/healthcheck.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, status -from src.schema.admin import HealthCheck +from src.schema.admin.healthcheck import HealthCheck health_router = APIRouter() diff --git a/kontor-api/src/apis/version1/media/mediaarticle.py b/kontor-api/src/apis/version1/media/mediaarticle.py new file mode 100644 index 0000000..cb19dc8 --- /dev/null +++ b/kontor-api/src/apis/version1/media/mediaarticle.py @@ -0,0 +1,25 @@ + +from typing import List + +from fastapi import APIRouter + +from src.db.models.media import MediaArticle +from src.db.session import SessionDep +from src.schema.media.article import MediaArticleResponse, to_response + +router = APIRouter() + +@router.get("/articles", response_model=List[MediaArticleResponse]) +def get_all_files( + db: SessionDep, review: bool = False, download: bool = False +) -> List[MediaArticleResponse]: + """ + Get all MediaVideos. + """ + results: List[MediaArticleResponse] = [] + articles: List[MediaArticle] + articles = db.query(MediaArticle).all() + for article in articles: + response = to_response(article) + results.append(response) + return results diff --git a/kontor-api/src/apis/version1/media/mediavideo.py b/kontor-api/src/apis/version1/media/mediavideo.py new file mode 100644 index 0000000..06e3252 --- /dev/null +++ b/kontor-api/src/apis/version1/media/mediavideo.py @@ -0,0 +1,31 @@ + +from typing import List + +from fastapi import APIRouter + +from src.db.models.media import MediaVideo +from src.db.session import SessionDep +from src.schema.media.video import MediaVideoResponse, to_response + + +router = APIRouter() + +@router.get("/videos", response_model=List[MediaVideoResponse]) +def get_all_files( + db: SessionDep, review: bool = False, download: bool = False +) -> List[MediaVideoResponse]: + """ + Get all MediaVideos. + """ + results: List[MediaVideoResponse] = [] + files: List[MediaVideo] + if review: + files = db.query(MediaVideo).filter(MediaVideo.review.is_(True)).all() + elif download: + files = db.query(MediaVideo).filter(MediaVideo.should_download.is_(True)).all() + else: + files = db.query(MediaVideo).all() + for mediafile in files: + response = to_response(mediafile) + results.append(response) + return results diff --git a/kontor-api/src/apis/version1/user/assignment.py b/kontor-api/src/apis/version1/user/assignment.py new file mode 100644 index 0000000..b9c0ed4 --- /dev/null +++ b/kontor-api/src/apis/version1/user/assignment.py @@ -0,0 +1,20 @@ +from typing import List + +from fastapi import APIRouter + +from src.db.models.admin import Assignment +from src.db.session import SessionDep +from src.schema.user.assignment import AssignmentResponse, to_response + + +router = APIRouter() + + +@router.get("/assignments", response_model=List[AssignmentResponse]) +def get_all_profiles(db: SessionDep) -> List[AssignmentResponse]: + results: List[AssignmentResponse] = [] + profiles = db.query(Assignment).all() + for profile in profiles: + response = to_response(profile) + results.append(response) + return results diff --git a/kontor-api/src/apis/version1/user/permission.py b/kontor-api/src/apis/version1/user/permission.py new file mode 100644 index 0000000..8e6e401 --- /dev/null +++ b/kontor-api/src/apis/version1/user/permission.py @@ -0,0 +1,19 @@ +from typing import List + +from fastapi import APIRouter + +from src.db.models.admin import Permission +from src.db.session import SessionDep +from src.schema.user.permission import PermissionResponse, to_response + +router = APIRouter() + + +@router.get("/permissions", response_model=List[PermissionResponse]) +def get_all_profiles(db: SessionDep) -> List[PermissionResponse]: + results: List[PermissionResponse] = [] + permissions = db.query(Permission).all() + for permission in permissions: + response = to_response(permission) + results.append(response) + return results diff --git a/kontor-api/src/apis/version1/user/profile.py b/kontor-api/src/apis/version1/user/profile.py index 2fd2797..991bcdb 100644 --- a/kontor-api/src/apis/version1/user/profile.py +++ b/kontor-api/src/apis/version1/user/profile.py @@ -3,6 +3,7 @@ from fastapi import APIRouter, HTTPException, status from sqlalchemy import select from src.core.log_conf import logger +from src.core.security import CurrentUser from src.db.models.admin import Profile from src.db.repository.user import create_new_profile, get_profile_details from src.db.session import SessionDep @@ -11,8 +12,13 @@ from src.schema.user.profile import ProfileResponse, ProfileModel router = APIRouter() + +@router.get("/profile", response_model=ProfileModel) +async def read_profile(current_user: CurrentUser): + return current_user + @router.get("/profiles", response_model=List[ProfileResponse]) -def get_all_profiles(db: SessionDep) -> List[ProfileResponse]: # type: ignore +def get_all_profiles(db: SessionDep) -> List[ProfileResponse]: results: List[ProfileResponse] = [] profiles = db.scalars(select(Profile)).all() for profile in profiles: @@ -21,7 +27,7 @@ def get_all_profiles(db: SessionDep) -> List[ProfileResponse]: # type: ignore return results @router.get("/profiles/{profile_id}", response_model=ProfileResponse) -def get_profile(profile_id: str, db: SessionDep) -> ProfileResponse: # type: ignore +def get_profile(profile_id: str, db: SessionDep) -> ProfileResponse: profile = db.get(Profile, profile_id) if not profile: raise HTTPException(status_code=404, detail="MediaActor could not be found") @@ -37,8 +43,8 @@ def delete_profile(profile_id: str, db: SessionDep): # type: ignore delete_profile(profile_id=profile_id, db=db) @router.post("/profiles", status_code=status.HTTP_201_CREATED) -def add_profile(new_profile: ProfileModel, db: SessionDep) -> ProfileResponse: # type: ignore - logger.info(f"add profile {new_profile.user_name}") +def add_profile(new_profile: ProfileModel, db: SessionDep) -> ProfileResponse: + logger.info(f"add profile {new_profile.username}") try: profile: Profile = create_new_profile(new_profile, db) except: diff --git a/kontor-api/src/apis/version1/user/token.py b/kontor-api/src/apis/version1/user/token.py new file mode 100644 index 0000000..f3a18ba --- /dev/null +++ b/kontor-api/src/apis/version1/user/token.py @@ -0,0 +1,19 @@ +from typing import List + +from fastapi import APIRouter + +from src.db.models.admin import Token +from src.db.session import SessionDep +from src.schema.user.token import TokenResponse, to_response + +router = APIRouter() + + +@router.get("/tokens", response_model=List[TokenResponse]) +def get_all_profiles(db: SessionDep) -> List[TokenResponse]: + results: List[TokenResponse] = [] + tokens = db.query(Token).all() + for token in tokens: + response = to_response(token) + results.append(response) + return results diff --git a/kontor-api/src/core/middleware.py b/kontor-api/src/core/middleware.py new file mode 100644 index 0000000..a0b86f9 --- /dev/null +++ b/kontor-api/src/core/middleware.py @@ -0,0 +1,35 @@ +import json +import time + +from fastapi import Request, Response +from starlette.middleware.base import BaseHTTPMiddleware +from src.core.log_conf import logger + +class RequestLoggingMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next) -> Response: + start_time = time.time() + path = request.url.path + + if path != "/health": + # Log request info + request_info ={ + "method": request.method, + "path": path, + "client_ip": request.client.host if request.client else "unknown" + } + logger.info("Incoming: %s", json.dumps(request_info)) + + # Process request + response = await call_next(request) + + if path != "/health": + # Log response info + duration_ms = (time.time()- start_time)*1000 + response_info = { + "status_code": response.status_code, + "duration_ms": round(duration_ms, 2) + } + logger.info("completed: %s", json.dumps(response_info)) + + return response + \ No newline at end of file diff --git a/kontor-api/src/core/security.py b/kontor-api/src/core/security.py index 9fb190c..aae17f0 100644 --- a/kontor-api/src/core/security.py +++ b/kontor-api/src/core/security.py @@ -1,12 +1,15 @@ -import logging from datetime import datetime, timedelta, timezone -from typing import Annotated, Dict, List, Optional +from typing import Annotated, List, Optional import bcrypt -from fastapi import Depends, HTTPException, Request, Security, status -from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel -from fastapi.security import OAuth2, OAuth2PasswordBearer, SecurityScopes -from fastapi.security.utils import get_authorization_scheme_param +from fastapi import Depends, HTTPException, Security, status +#from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel +from fastapi.security import ( + #OAuth2, + OAuth2PasswordBearer, + SecurityScopes +) +#from fastapi.security.utils import get_authorization_scheme_param from jose import JWTError, jwt from pydantic import ValidationError @@ -19,7 +22,8 @@ from src.db.repository.admin import ( is_database_empty, ) from src.db.session import SessionLocal -from src.schema.admin import ProfileModel, TokenData +from src.schema.admin.token import TokenData +from src.schema.user.profile import ProfileModel, to_model oauth2_scheme = OAuth2PasswordBearer( tokenUrl="/token", @@ -161,13 +165,7 @@ async def get_current_active_user( ) -> ProfileModel: if not current_user.enabled: raise HTTPException(status_code=400, detail="Inactive user") - user_model = ProfileModel( - username=current_user.user_name, - email=current_user.email, # type: ignore - first_name=current_user.first_name, - last_name=current_user.last_name, # type: ignore - active=current_user.enabled, - ) # type: ignore + user_model = to_model(current_user) return user_model @@ -181,7 +179,7 @@ def get_current_user_from_token(token: str = Depends(oauth2_scheme)): payload = jwt.decode( token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM] ) - username: str = payload.get("sub") # type: ignore + username: Optional[str] = payload.get("sub") logger.info("username/email extracted is %s", username) if username is None: raise credentials_exception @@ -190,8 +188,12 @@ def get_current_user_from_token(token: str = Depends(oauth2_scheme)): with SessionLocal() as db: user = get_profile_by_email(email=username, db=db) if user is None: - raise credentials_exception + user = get_profile_by_username(username=username, db=db) + if user is None: + raise credentials_exception return user UserDep = Annotated[Profile, Depends(get_current_user_from_token)] + +CurrentUser = Annotated[Profile, Depends(get_current_active_user)] diff --git a/kontor-api/src/db/repository/user.py b/kontor-api/src/db/repository/user.py index 16c1d0a..968f238 100644 --- a/kontor-api/src/db/repository/user.py +++ b/kontor-api/src/db/repository/user.py @@ -38,7 +38,7 @@ def delete_profile(db: Session, profile_id: str): def get_profile_details(profile: Profile) -> ProfileResponse: reponse: ProfileResponse = ProfileResponse( id=profile.id, - user_name=str(profile.user_name) + username=str(profile.user_name) ) return reponse diff --git a/kontor-api/src/main.py b/kontor-api/src/main.py index 3d48256..f090518 100644 --- a/kontor-api/src/main.py +++ b/kontor-api/src/main.py @@ -9,6 +9,7 @@ from src.apis.version1.healthcheck import health_router from src.apis.version1.admin.login import login_router from src.core.config import settings from src.core.log_conf import logger +from src.core.middleware import RequestLoggingMiddleware from src.db.models.base import Base from src.db.session import engine from src.db.utils import check_db_connected, check_db_disconnected @@ -23,10 +24,10 @@ async def lifespan(app: FastAPI): def include_router(app: FastAPI): - app.include_router(api_router) - app.include_router(web_app_router) - app.include_router(health_router) - app.include_router(login_router) + app.include_router(api_router, tags=["api"]) + app.include_router(web_app_router, tags=["webapp"]) + app.include_router(health_router, tags=["admin"]) + app.include_router(login_router, tags=["webapp"]) def configure_static(app: FastAPI): @@ -41,6 +42,7 @@ def add_middle_ware(app: FastAPI): allow_methods=["*"], allow_headers=["*"], ) + app.add_middleware(RequestLoggingMiddleware) def create_tables(): diff --git a/kontor-api/src/schema/admin/healthcheck.py b/kontor-api/src/schema/admin/healthcheck.py new file mode 100644 index 0000000..1ee4e6f --- /dev/null +++ b/kontor-api/src/schema/admin/healthcheck.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +class HealthCheck(BaseModel): + """ + Health check model + """ + + status: str = "ok" diff --git a/kontor-api/src/schema/admin/login.py b/kontor-api/src/schema/admin/login.py new file mode 100644 index 0000000..f9eef3e --- /dev/null +++ b/kontor-api/src/schema/admin/login.py @@ -0,0 +1,8 @@ +from typing import Optional + +from pydantic import BaseModel + + +class LoginRequest(BaseModel): + email: Optional[str] = None + password: Optional[str] = None diff --git a/kontor-api/src/schema/admin.py b/kontor-api/src/schema/admin/mailaccount.py similarity index 67% rename from kontor-api/src/schema/admin.py rename to kontor-api/src/schema/admin/mailaccount.py index edbdcc8..061a467 100644 --- a/kontor-api/src/schema/admin.py +++ b/kontor-api/src/schema/admin/mailaccount.py @@ -1,37 +1,10 @@ from datetime import datetime -from typing import Optional, List from pydantic import BaseModel from src.db.models.admin import MailAccount -class Token(BaseModel): - access_token: str - token_type: str - - -class TokenData(BaseModel): - username: Optional[str] = None - scopes: List[str] = [] - - -class ProfileModel(BaseModel): - username: str - email: str - first_name: str - last_name: str - active: bool - - -class HealthCheck(BaseModel): - """ - Health check model - """ - - status: str = "ok" - - class MailAccountResponse(BaseModel): id: str created_date: datetime diff --git a/kontor-api/src/schema/admin/token.py b/kontor-api/src/schema/admin/token.py new file mode 100644 index 0000000..47b21b6 --- /dev/null +++ b/kontor-api/src/schema/admin/token.py @@ -0,0 +1,13 @@ +from typing import List, Optional + +from pydantic import BaseModel + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Optional[str] = None + scopes: List[str] = [] diff --git a/kontor-api/src/schema/comics/issue.py b/kontor-api/src/schema/comics/issue.py index 719114c..e4485bc 100644 --- a/kontor-api/src/schema/comics/issue.py +++ b/kontor-api/src/schema/comics/issue.py @@ -1,8 +1,38 @@ +from datetime import datetime +from typing import Optional + from pydantic import BaseModel +from src.db.models.comic import Issue + class IssueResponse(BaseModel): id: str + created_date: datetime + last_modified_date: datetime + version: int issue_number: str + title: Optional[str] + published_on: Optional[datetime] in_stock: bool is_read: bool + comic_id: str + volume_id: Optional[str] + story_arc_id: Optional[str] + +def to_response(issue: Issue) -> IssueResponse: + response: IssueResponse = IssueResponse( + id=issue.id, + created_date=issue.created_date, + last_modified_date=issue.last_modified_date, + version=issue.version, + issue_number=issue.issue_number, + title=issue.title, + published_on=issue.published_on, + in_stock=issue.in_stock, + is_read=issue.is_read, + comic_id=issue.comic_id, + volume_id=issue.volume_id, + story_arc_id=issue.story_arc_id + ) + return response diff --git a/kontor-api/src/schema/comics/issue_details.py b/kontor-api/src/schema/comics/issue_details.py index 3c39799..df9c79d 100644 --- a/kontor-api/src/schema/comics/issue_details.py +++ b/kontor-api/src/schema/comics/issue_details.py @@ -11,3 +11,4 @@ class IssueDetailsResponse(BaseModel): is_read: bool comic: ComicResponse volume: VolumeResponse | None + diff --git a/kontor-api/src/schema/comics/storyarc.py b/kontor-api/src/schema/comics/storyarc.py new file mode 100644 index 0000000..c18f411 --- /dev/null +++ b/kontor-api/src/schema/comics/storyarc.py @@ -0,0 +1,30 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel + +from src.db.models.comic import StoryArc + +class StoryArcResponse(BaseModel): + id: str + created_date: datetime + last_modified_date: datetime + version: int + name: str + comic_id: str + volume_id: Optional[str] + +class AddLink(BaseModel): + url: str + +def to_response(storyarc: StoryArc) -> StoryArcResponse: + response: StoryArcResponse = StoryArcResponse( + id=storyarc.id, + created_date=storyarc.created_date, + last_modified_date=storyarc.last_modified_date, + version=storyarc.version, + name=storyarc.name, + comic_id=storyarc.comic_id, + volume_id=storyarc.volume_id + ) + return response diff --git a/kontor-api/src/schema/comics/volume.py b/kontor-api/src/schema/comics/volume.py index 81d7985..1f769b0 100644 --- a/kontor-api/src/schema/comics/volume.py +++ b/kontor-api/src/schema/comics/volume.py @@ -1,6 +1,25 @@ +from datetime import datetime + from pydantic import BaseModel +from src.db.models.comic import Volume + class VolumeResponse(BaseModel): id: str + created_date: datetime + last_modified_date: datetime + version: int name: str + comic_id: str + +def to_response(volume: Volume) -> VolumeResponse: + response: VolumeResponse = VolumeResponse( + id=volume.id, + created_date=volume.created_date, + last_modified_date=volume.last_modified_date, + version=volume.version, + name=volume.name, + comic_id=volume.comic_id + ) + return response diff --git a/kontor-api/src/schema/comics/worktype.py b/kontor-api/src/schema/comics/worktype.py index a072938..19ba7c7 100644 --- a/kontor-api/src/schema/comics/worktype.py +++ b/kontor-api/src/schema/comics/worktype.py @@ -1,9 +1,26 @@ +from datetime import datetime + from pydantic import BaseModel +from src.db.models.comic import WorkType + class AddWorkType(BaseModel): worktype: str class WorktypeResponse(BaseModel): id: str + created_date: datetime + last_modified_date: datetime + version: int name: str + +def to_response(worktype: WorkType) -> WorktypeResponse: + response: WorktypeResponse = WorktypeResponse( + id=worktype.id, + created_date=worktype.created_date, + last_modified_date=worktype.last_modified_date, + version=worktype.version, + name=worktype.name + ) + return response diff --git a/kontor-api/src/schema/media/article.py b/kontor-api/src/schema/media/article.py new file mode 100644 index 0000000..d4a1d79 --- /dev/null +++ b/kontor-api/src/schema/media/article.py @@ -0,0 +1,30 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel + +from src.db.models.media import MediaArticle + +class MediaArticleResponse(BaseModel): + id: str + created_date: datetime + last_modified_date: datetime + version: int + review: bool = False + title: Optional[str] = None + url: Optional[str] = None + +class AddLink(BaseModel): + url: str + +def to_response(video: MediaArticle) -> MediaArticleResponse: + response: MediaArticleResponse = MediaArticleResponse( + id=video.id, + created_date=video.created_date, + last_modified_date=video.last_modified_date, + version=video.version, + review=video.review, + title=video.title, + url=video.url, + ) + return response diff --git a/kontor-api/src/schema/media/video.py b/kontor-api/src/schema/media/video.py index ed4319a..e07f1ef 100644 --- a/kontor-api/src/schema/media/video.py +++ b/kontor-api/src/schema/media/video.py @@ -1,5 +1,38 @@ +from datetime import datetime +from typing import Optional + from pydantic import BaseModel +from src.db.models.media import MediaVideo + +class MediaVideoResponse(BaseModel): + id: str + created_date: datetime + last_modified_date: datetime + version: int + cloud_link: Optional[str] = None + file_name: Optional[str] = None + path: Optional[str] = None + review: bool = False + title: Optional[str] = None + url: Optional[str] = None + should_download: bool = False class AddLink(BaseModel): url: str + +def to_response(video: MediaVideo) -> MediaVideoResponse: + response: MediaVideoResponse = MediaVideoResponse( + id=video.id, + created_date=video.created_date, + last_modified_date=video.last_modified_date, + version=video.version, + cloud_link=video.cloud_link, + file_name=video.file_name, + path=video.path, + review=video.review, + title=video.title, + url=video.url, + should_download=video.should_download + ) + return response diff --git a/kontor-api/src/schema/user/assignment.py b/kontor-api/src/schema/user/assignment.py new file mode 100644 index 0000000..bb12a66 --- /dev/null +++ b/kontor-api/src/schema/user/assignment.py @@ -0,0 +1,26 @@ +from datetime import datetime + +from pydantic import BaseModel + +from src.db.models.admin import Assignment + + +class AssignmentResponse(BaseModel): + id: str + created_date: datetime + last_modified_date: datetime + version: int + profile_id: str + permission_id: str + + +def to_response(assignment: Assignment) -> AssignmentResponse: + response: AssignmentResponse = AssignmentResponse( + id=assignment.id, + created_date=assignment.created_date, + last_modified_date=assignment.last_modified_date, + version=assignment.version, + profile_id=assignment.profile_id, + permission_id=assignment.permission_id + ) + return response diff --git a/kontor-api/src/schema/user/permission.py b/kontor-api/src/schema/user/permission.py new file mode 100644 index 0000000..913e0e0 --- /dev/null +++ b/kontor-api/src/schema/user/permission.py @@ -0,0 +1,24 @@ +from datetime import datetime + +from pydantic import BaseModel + +from src.db.models.admin import Permission + + +class PermissionResponse(BaseModel): + id: str + created_date: datetime + last_modified_date: datetime + version: int + name: str + + +def to_response(permission: Permission) -> PermissionResponse: + response: PermissionResponse = PermissionResponse( + id=permission.id, + created_date=permission.created_date, + last_modified_date=permission.last_modified_date, + version=permission.version, + name=permission.name + ) + return response diff --git a/kontor-api/src/schema/user/profile.py b/kontor-api/src/schema/user/profile.py index 1b5d0d4..672145b 100644 --- a/kontor-api/src/schema/user/profile.py +++ b/kontor-api/src/schema/user/profile.py @@ -1,12 +1,26 @@ from pydantic import BaseModel +from src.db.models.admin import Profile + class ProfileResponse(BaseModel): id: str - user_name: str + username: str class ProfileModel(BaseModel): - user_name: str + username: str + email: str first_name: str last_name: str + active: bool + +def to_model(profile: Profile) -> ProfileModel: + model: ProfileModel = ProfileModel( + username=profile.user_name, + email=profile.email, + first_name=profile.first_name, + last_name=profile.last_name, + active=profile.enabled, + ) + return model diff --git a/kontor-api/src/schema/user/token.py b/kontor-api/src/schema/user/token.py new file mode 100644 index 0000000..4748a74 --- /dev/null +++ b/kontor-api/src/schema/user/token.py @@ -0,0 +1,32 @@ +from datetime import datetime + +from pydantic import BaseModel + +from src.db.models.admin import Token + + +class TokenResponse(BaseModel): + id: str + created_date: datetime + last_modified_date: datetime + version: int + token: str + name: str + last_used_date: datetime + enabled: bool + profile_id: str + + +def to_response(token: Token) -> TokenResponse: + response: TokenResponse = TokenResponse( + id=token.id, + created_date=token.created_date, + last_modified_date=token.last_modified_date, + version=token.version, + token=token.token, + name=token.name, + last_used_date=token.last_used_date, + enabled=token.enabled, + profile_id=token.profile_id + ) + return response diff --git a/kontor-api/src/webapps/auth/route_login.py b/kontor-api/src/webapps/auth/route_login.py index 4373afa..5d65a67 100644 --- a/kontor-api/src/webapps/auth/route_login.py +++ b/kontor-api/src/webapps/auth/route_login.py @@ -1,5 +1,5 @@ -from src.apis.version1.admin.token import login_for_token_cookie -from fastapi import APIRouter, Depends +from src.apis.version1.admin.login import login_for_token_cookie +from fastapi import APIRouter from fastapi import HTTPException from fastapi import Request from fastapi.templating import Jinja2Templates diff --git a/kontor-api/src/webapps/base.py b/kontor-api/src/webapps/base.py index 3206a94..42ba071 100644 --- a/kontor-api/src/webapps/base.py +++ b/kontor-api/src/webapps/base.py @@ -9,15 +9,15 @@ from src.webapps.media import route_actors, route_media, route_videos templates = Jinja2Templates(directory="src/templates") api_router = APIRouter() -api_router.include_router(route_comics.router) -api_router.include_router(route_artists.router) -api_router.include_router(route_worktype.router) -api_router.include_router(route_media.router) -api_router.include_router(route_actors.router) -api_router.include_router(route_videos.router) -api_router.include_router(route_login.router) -api_router.include_router(route_admin.router) +api_router.include_router(route_comics.router, tags=["webapp"]) +api_router.include_router(route_artists.router, tags=["webapp"]) +api_router.include_router(route_worktype.router, tags=["webapp"]) +api_router.include_router(route_media.router, tags=["webapp"]) +api_router.include_router(route_actors.router, tags=["webapp"]) +api_router.include_router(route_videos.router, tags=["webapp"]) +api_router.include_router(route_login.router, tags=["webapp"]) +api_router.include_router(route_admin.router, tags=["webapp"]) -@api_router.get("/") +@api_router.get("/", tags=["webapp"]) def home(request: Request, msg: str | None = None): return templates.TemplateResponse("index.html", {"request": request, "msg": msg}) diff --git a/kontor-api/src/webapps/comic/route_comics.py b/kontor-api/src/webapps/comic/route_comics.py index 6dd4725..9285273 100644 --- a/kontor-api/src/webapps/comic/route_comics.py +++ b/kontor-api/src/webapps/comic/route_comics.py @@ -11,7 +11,7 @@ from src.schema.comics.comic import ComicSchema from src.webapps.comic.forms.comic import ValidateComicForm templates = Jinja2Templates(directory="src/templates") -router = APIRouter(include_in_schema=False, prefix="/comic") +router = APIRouter(include_in_schema=True, prefix="/comic") @router.get("/comics") def get_comics(db: SessionDep, request: Request, msg: str | None = None): @@ -58,7 +58,7 @@ async def validate_comic(request: Request, db: SessionDep, comic_id: str, action if form.is_valid(): try: comic = ComicSchema(**form.__dict__) - comic = update_comic(comic=comic, comic_id=comic_id, db=db) + comic = update_comic(new_comic=comic, comic_id=comic_id, db=db) return RedirectResponse(f"/comic/comics/{comic.id}", status_code=status.HTTP_303_SEE_OTHER) except Exception as e: print(e) diff --git a/kontor-api/tests/conftest.py b/kontor-api/tests/conftest.py index 6a1146e..d3afb43 100644 --- a/kontor-api/tests/conftest.py +++ b/kontor-api/tests/conftest.py @@ -8,7 +8,7 @@ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import Session, sessionmaker from src.apis.base import api_router from src.db.models.base import Base @@ -45,7 +45,7 @@ def app() -> Generator[FastAPI, Any, None]: @pytest.fixture(scope="module") -def db_session(app: FastAPI) -> Generator[SessionTesting, Any, None]: +def db_session(app: FastAPI) -> Generator[Session, Any, None]: connection = engine.connect() transaction = connection.begin() session = SessionTesting(bind=connection) @@ -57,7 +57,7 @@ def db_session(app: FastAPI) -> Generator[SessionTesting, Any, None]: @pytest.fixture(scope="module") def client( - app: FastAPI, db_session: SessionTesting + app: FastAPI, db_session: Session ) -> Generator[TestClient, Any, None]: """ Create a new FastAPI TestClient that uses the `db_session` fixture to override diff --git a/kontor-scripts/api.py b/kontor-scripts/api.py index 42c05c6..1a1b2c5 100644 --- a/kontor-scripts/api.py +++ b/kontor-scripts/api.py @@ -43,7 +43,7 @@ MAPPING: Dict[str, str] = { "permission": "api/user/permissions", "assignment": "api/user/assignments", "token": "api/user/tokens", - "mail_account": "api/admin/mailaccount", + "mail_account": "api/admin/mailaccounts", } class EndPointNotAvailableException(Exception): diff --git a/kontor-scripts/sync.py b/kontor-scripts/sync.py index 0ddb0e9..58bbf90 100644 --- a/kontor-scripts/sync.py +++ b/kontor-scripts/sync.py @@ -28,8 +28,8 @@ if __name__== "__main__": try: data = server.request(logger, table=table) logger.info("%s: %s", table, len(data)) - if len(data) == 1: - logger.info("show data: %s", data) + #if len(data) == 1: + # logger.info("show data: %s", data) except EndPointNotAvailableException: logger.info("Endpoint not implemented") logger.info("kontor.sync finished")