diff --git a/kontor-api/pyproject.toml b/kontor-api/pyproject.toml index 672f36b..978b9a5 100644 --- a/kontor-api/pyproject.toml +++ b/kontor-api/pyproject.toml @@ -24,4 +24,9 @@ dependencies = [ "natsort>=8.4.0", "psycopg2-binary>=2.9.10", "pytest-cov>=6.1.1", + "databases[sqlite]>=0.9.0", + "pydantic[email]>=2.11.3", + "jinja2>=3.1.6", + "asyncpg>=0.30.0", + "bcrypt>=4.3.0", ] diff --git a/kontor-api/src/apis/base.py b/kontor-api/src/apis/base.py index 6c92e7f..35faadd 100644 --- a/kontor-api/src/apis/base.py +++ b/kontor-api/src/apis/base.py @@ -1,8 +1,10 @@ from fastapi import APIRouter -from src.apis.version1 import comic, media, tysc +from src.apis.version1 import comic, media, tysc, admin, metadata api_router = APIRouter(prefix="/api") -api_router.include_router(comic.router, tags=["comics"]) -api_router.include_router(media.router, tags=["media"]) -api_router.include_router(tysc.router, tags=["tysc"]) +api_router.include_router(comic.router, prefix="/comics", tags=["comics"]) +api_router.include_router(media.router, prefix="/media", tags=["media"]) +api_router.include_router(tysc.router, prefix="/tysc", tags=["tysc"]) +api_router.include_router(admin.router, prefix="/login", tags=["login"]) +api_router.include_router(metadata.router, prefix="/metadata", tags=["metadata"]) diff --git a/kontor-api/src/apis/utils.py b/kontor-api/src/apis/utils.py index 716d2be..6d1ff22 100644 --- a/kontor-api/src/apis/utils.py +++ b/kontor-api/src/apis/utils.py @@ -1,4 +1,13 @@ from typing import Annotated +from typing import Dict +from typing import Optional + +from fastapi import HTTPException +from fastapi import Request +from fastapi import status +from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel +from fastapi.security import OAuth2 +from fastapi.security.utils import get_authorization_scheme_param from fastapi import Depends from sqlalchemy.orm import Session @@ -6,3 +15,34 @@ from sqlalchemy.orm import Session from src.db.session import get_db SessionDep = Annotated[Session, Depends(get_db)] + + +class OAuth2PasswordBearerWithCookie(OAuth2): + def __init__( + self, + tokenUrl: str, + scheme_name: Optional[str] = None, + scopes: Optional[Dict[str, str]] = None, + auto_error: bool = True, + ): + if not scopes: + scopes = {} + flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes}) + super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error) + + async def __call__(self, request: Request) -> Optional[str]: + authorization: str = request.cookies.get( + "access_token" + ) # changed to accept access token from httpOnly Cookie + + scheme, param = get_authorization_scheme_param(authorization) + if not authorization or scheme.lower() != "bearer": + if self.auto_error: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, + ) + else: + return None + return param diff --git a/kontor-api/src/apis/version1/admin.py b/kontor-api/src/apis/version1/admin.py new file mode 100644 index 0000000..4c7a626 --- /dev/null +++ b/kontor-api/src/apis/version1/admin.py @@ -0,0 +1,69 @@ +import logging +from datetime import timedelta + +import bcrypt +from fastapi import APIRouter, HTTPException, status, Response, Depends +from fastapi.security import OAuth2PasswordRequestForm +from jose import jwt, JWTError +from src.apis.utils import SessionDep, OAuth2PasswordBearerWithCookie +from src.core.config import settings +from src.core.security import create_access_token +from src.db.models.admin import Profile +from src.db.repository.admin import get_profile +from src.schema.admin import Token + +router = APIRouter() + + +def authenticate_user(username: str, password: str, db: SessionDep) -> Profile | None: + user = get_profile(username=username, db=db) + print(user) + if not user: + return None + if bcrypt.checkpw(password.encode(), user.password.encode()): + print("User successful authenticated") + else: + logging.info("Authentication failed!") + return user + + +@router.post("/token", response_model=Token) +def login_for_access_token(response: Response, db: SessionDep, form_data: OAuth2PasswordRequestForm = Depends()): + user = authenticate_user(form_data.username, form_data.password, db) + 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"} + + +oauth2_scheme = OAuth2PasswordBearerWithCookie(tokenUrl="/api/login/token") + + +def get_current_user_from_token(db: SessionDep, token: str = Depends(oauth2_scheme)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + ) + try: + payload = jwt.decode( + token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM] + ) + username: str = payload.get("sub") + print("username/email extracted is ", username) + if username is None: + raise credentials_exception + except JWTError: + raise credentials_exception + user = get_profile(username=username, db=db) + if user is None: + raise credentials_exception + return user diff --git a/kontor-api/src/apis/version1/comic.py b/kontor-api/src/apis/version1/comic.py index 5a70195..8feef55 100644 --- a/kontor-api/src/apis/version1/comic.py +++ b/kontor-api/src/apis/version1/comic.py @@ -8,11 +8,7 @@ from src.schema.comics.artist import ArtistCreation, ArtistDetailResponse, Artis from src.db.models.comic import Comic, Artist, Issue from src.schema.comics.issue import IssueDetailsResponse -router = APIRouter( - prefix="/comic", - tags=["comics"], - responses={404: {"description": "Not found"}}, -) +router = APIRouter() @router.get("/comics") diff --git a/kontor-api/src/apis/version1/media.py b/kontor-api/src/apis/version1/media.py index f6b4b7e..072097a 100644 --- a/kontor-api/src/apis/version1/media.py +++ b/kontor-api/src/apis/version1/media.py @@ -1,22 +1,20 @@ from typing import List, AnyStr -from uuid import UUID -from fastapi import APIRouter, status, HTTPException +from fastapi import APIRouter, status, HTTPException, Depends from sqlalchemy import select, Sequence from src.apis.utils import SessionDep +from src.apis.version1.admin import get_current_user_from_token +from src.db.models.admin import Profile from src.schema.media.file import MediaFileResponse, Link, get_file_details, set_file from src.db.models.media import MediaFile -router = APIRouter( - prefix="/media", - tags=["media"] -) +router = APIRouter() @router.get("/update-titles") def update_titles(db: SessionDep) -> list[MediaFileResponse]: results: list[MediaFileResponse] = [] - files = db.query(MediaFile).filter(MediaFile.review == 1).all() + files = db.query(MediaFile).filter(MediaFile.review == True).all() for mediafile in files: mediafile.update_title() db.add(mediafile) @@ -27,13 +25,13 @@ def update_titles(db: SessionDep) -> list[MediaFileResponse]: @router.get("/files", response_model=List[MediaFileResponse]) -def get_all_files(db: SessionDep, review: bool = False, download: bool = False) -> List[MediaFileResponse]: +def get_all_files(db: SessionDep, review: bool = False, download: bool = False, current_user: Profile = Depends(get_current_user_from_token)) -> List[MediaFileResponse]: results: list[MediaFileResponse] = [] files: Sequence[MediaFile] if review: - files = db.query(MediaFile).filter(MediaFile.review == 1).all() + files = db.query(MediaFile).filter(MediaFile.review == True).all() elif download: - files = db.query(MediaFile).filter(MediaFile.should_download == 1).all() + files = db.query(MediaFile).filter(MediaFile.should_download == True).all() else: files = db.scalars(select(MediaFile)).all() for mediafile in files: @@ -66,8 +64,8 @@ def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse: try: mediaFile: MediaFile = MediaFile() setattr(mediaFile, "url", new_link.url) - setattr(mediaFile, "review", 1) - setattr(mediaFile, "should_download", 1) + setattr(mediaFile, "review", True) + setattr(mediaFile, "should_download", True) db.add(mediaFile) db.commit() except: diff --git a/kontor-api/src/apis/version1/metadata.py b/kontor-api/src/apis/version1/metadata.py new file mode 100644 index 0000000..2860880 --- /dev/null +++ b/kontor-api/src/apis/version1/metadata.py @@ -0,0 +1,26 @@ +from typing import List + +from fastapi import APIRouter + +from src.apis.utils import SessionDep +from src.db.models.metadata import MetaDataTable, MetaDataColumn +from src.db.repository.metadata import get_tables, get_columns +from src.schema.admin import MetaDataTableResponse, MetaDataColumnResponse + +router = APIRouter() + + + +@router.get("/tables") +def get_meta_data_tables(db: SessionDep) -> List[MetaDataTableResponse]: + tables = db.query(MetaDataTable).all() + response: List[MetaDataTableResponse] = get_tables(tables) + return response + + + +@router.get("/columns") +def get_meta_data_columns(db: SessionDep) -> List[MetaDataColumnResponse]: + columns = db.query(MetaDataColumn).all() + response: List[MetaDataColumnResponse] = get_columns(columns) + return response diff --git a/kontor-api/src/apis/version1/tysc.py b/kontor-api/src/apis/version1/tysc.py index 1128771..c8b8bb2 100644 --- a/kontor-api/src/apis/version1/tysc.py +++ b/kontor-api/src/apis/version1/tysc.py @@ -5,11 +5,7 @@ from src.apis.utils import SessionDep from src.schema.tysc.sport import SportResponse from src.db.models.tysc import Sport -router = APIRouter( - prefix="/tysc", - tags=["tysc"], - responses={404: {"description": "Not found"}}, -) +router = APIRouter() @router.get("/sports") def get_all_sports(db: SessionDep) -> List[SportResponse]: diff --git a/kontor-api/src/core/config.py b/kontor-api/src/core/config.py index a5e36c1..81b5fa0 100644 --- a/kontor-api/src/core/config.py +++ b/kontor-api/src/core/config.py @@ -17,7 +17,9 @@ class Settings: DB_PORT: str = os.getenv("DB_PORT", 5432) DB_DBNAME: str = os.getenv("DB_DBNAME", "kontor") DATABASE_URL: str = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_SERVER}:{DB_PORT}/{DB_DBNAME}" - + SECRET_KEY: str = os.getenv("SECRET_KEY", "J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc=") + ALGORITHM = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES = 30 # in mins settings = Settings() diff --git a/kontor-api/src/core/security.py b/kontor-api/src/core/security.py new file mode 100644 index 0000000..9d5c588 --- /dev/null +++ b/kontor-api/src/core/security.py @@ -0,0 +1,21 @@ +from datetime import datetime +from datetime import timedelta +from typing import Optional + +from src.core.config import settings +from jose import jwt + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta( + minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES + ) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode( + to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM + ) + return encoded_jwt diff --git a/kontor-api/src/db/models/admin.py b/kontor-api/src/db/models/admin.py index 50f0656..6069b79 100644 --- a/kontor-api/src/db/models/admin.py +++ b/kontor-api/src/db/models/admin.py @@ -27,6 +27,9 @@ class Profile(Base, BaseMixin): full_name += self.last_name return full_name + def __str__(self): + return f"Profile({self.id} {self.user_name}, {self.email})" + class Token(Base, BaseMixin): __tablename__ = "token" diff --git a/kontor-api/src/db/models/database.py b/kontor-api/src/db/models/database.py index 9a69627..0da5f88 100644 --- a/kontor-api/src/db/models/database.py +++ b/kontor-api/src/db/models/database.py @@ -1,10 +1,9 @@ import json import logging -import uuid from datetime import datetime from enum import Enum, auto from pathlib import Path -from typing import Any +from typing import Any, List from sqlalchemy import select from sqlalchemy.exc import IntegrityError @@ -360,7 +359,7 @@ class KontorDB: update_list[link.id] = link.title return update_list - def get_download_list(self) -> list[uuid.UUID]: + def get_download_list(self) -> List[str]: download_list = [] __session__ = sessionmaker(self.engine) _filter = { 'should_download': True} diff --git a/kontor-api/src/db/repository/admin.py b/kontor-api/src/db/repository/admin.py new file mode 100644 index 0000000..a2f15e0 --- /dev/null +++ b/kontor-api/src/db/repository/admin.py @@ -0,0 +1,10 @@ +from typing import AnyStr + +from sqlalchemy.orm import Session + +from src.db.models.admin import Profile + + +def get_profile(username: AnyStr, db: Session): + profile = db.query(Profile).filter(Profile.email == username).first() + return profile diff --git a/kontor-api/src/db/repository/metadata.py b/kontor-api/src/db/repository/metadata.py new file mode 100644 index 0000000..86a7d04 --- /dev/null +++ b/kontor-api/src/db/repository/metadata.py @@ -0,0 +1,32 @@ +from typing import List + +from src.db.models.metadata import MetaDataTable, MetaDataColumn +from src.schema.admin import MetaDataTableResponse, MetaDataColumnResponse + + +def get_tables(tables: List[MetaDataTable]) -> List[MetaDataTableResponse]: + results: List[MetaDataTableResponse] = [] + for table in tables: + result = MetaDataTableResponse(id=table.id, name=table.table_name) + results.append(result) + return results + +def get_columns(columns: List[MetaDataColumn]) -> List[MetaDataColumnResponse]: + results: List[MetaDataColumnResponse] = [] + for column in columns: + result = MetaDataColumnResponse( + id=column.id, + table_name=column.table.table_name, + column_name=column.column_name, + column_sync_name=column.column_sync_name, + column_type=column.column_type, + column_modifier=column.column_modifier, + column_order=column.column_order, + is_shown=column.is_shown, + column_label=column.column_label, + show_filter=column.show_filter, + filter_label=column.filter_label, + ref_column=column.ref_column + ) + results.append(result) + return results diff --git a/kontor-api/src/db/utils.py b/kontor-api/src/db/utils.py new file mode 100644 index 0000000..8914088 --- /dev/null +++ b/kontor-api/src/db/utils.py @@ -0,0 +1,28 @@ +import databases +from src.db.session import SQLALCHEMY_DATABASE_URL + + +async def check_db_connected(): + try: + if not str(SQLALCHEMY_DATABASE_URL).__contains__("sqlite"): + database = databases.Database(SQLALCHEMY_DATABASE_URL) + if not database.is_connected: + await database.connect() + await database.execute("SELECT 1") + print("Database is connected (^_^)") + except Exception as e: + print( + "Looks like db is missing or is there is some problem in connection,see below traceback" + ) + raise e + + +async def check_db_disconnected(): + try: + if not str(SQLALCHEMY_DATABASE_URL).__contains__("sqlite"): + database = databases.Database(SQLALCHEMY_DATABASE_URL) + if database.is_connected: + await database.disconnect() + print("Database is Disconnected (-_-) zZZ") + except Exception as e: + raise e diff --git a/kontor-api/src/main.py b/kontor-api/src/main.py index 759c758..df4abe9 100644 --- a/kontor-api/src/main.py +++ b/kontor-api/src/main.py @@ -1,10 +1,12 @@ import logging +from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from src.apis.base import api_router from src.db.session import engine +from src.db.utils import check_db_connected, check_db_disconnected from src.webapps.base import api_router as web_app_router from src.core.config import settings from src.db.models.base import Base @@ -13,6 +15,12 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[logging.StreamHandler()]) # Logs to console +@asynccontextmanager +async def lifespan(app: FastAPI): + await check_db_connected() + yield + await check_db_disconnected() + def include_router(app: FastAPI): app.include_router(api_router) app.include_router(web_app_router) @@ -25,7 +33,7 @@ def create_tables(): def start_application(): logging.info(f"using database: {settings.DATABASE_URL}") - app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION) + app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION, lifespan=lifespan) include_router(app) configure_static(app) create_tables() diff --git a/kontor-api/src/schema/admin.py b/kontor-api/src/schema/admin.py new file mode 100644 index 0000000..25beb3c --- /dev/null +++ b/kontor-api/src/schema/admin.py @@ -0,0 +1,26 @@ +from typing import Optional + +from pydantic import BaseModel + + +class Token(BaseModel): + access_token: str + token_type: str + +class MetaDataTableResponse(BaseModel): + id: str + name: str + +class MetaDataColumnResponse(BaseModel): + id: str + table_name: str + column_name: str + column_sync_name: str + column_type: str + column_modifier: Optional[str] + column_order: int + is_shown: bool + column_label: Optional[str] + show_filter: bool + filter_label: Optional[str] + ref_column: Optional[str] diff --git a/kontor-api/src/schema/comics/artist.py b/kontor-api/src/schema/comics/artist.py index 925fa56..c97282c 100644 --- a/kontor-api/src/schema/comics/artist.py +++ b/kontor-api/src/schema/comics/artist.py @@ -1,9 +1,7 @@ from typing import List, Dict -from uuid import UUID from pydantic import BaseModel -from src.db.models.comic import Artist class ArtistCreation(BaseModel): diff --git a/kontor-api/src/schema/comics/comic.py b/kontor-api/src/schema/comics/comic.py index 884e122..4d7051b 100644 --- a/kontor-api/src/schema/comics/comic.py +++ b/kontor-api/src/schema/comics/comic.py @@ -1,5 +1,4 @@ from typing import List, Dict -from uuid import UUID from pydantic import BaseModel diff --git a/kontor-api/src/schema/media/file.py b/kontor-api/src/schema/media/file.py index 44f6b4f..5ad76df 100644 --- a/kontor-api/src/schema/media/file.py +++ b/kontor-api/src/schema/media/file.py @@ -1,12 +1,12 @@ from datetime import datetime -from uuid import UUID +from typing import AnyStr from src.db.models.media import MediaFile from pydantic import BaseModel class MediaFileResponse(BaseModel): - id: UUID + id: AnyStr title: str | None = None file_name: str | None = None cloud_link: str | None = None diff --git a/kontor-api/src/schema/tysc/sport.py b/kontor-api/src/schema/tysc/sport.py index 8ddbfb3..017d169 100644 --- a/kontor-api/src/schema/tysc/sport.py +++ b/kontor-api/src/schema/tysc/sport.py @@ -1,8 +1,8 @@ -from uuid import UUID +from typing import AnyStr from pydantic import BaseModel class SportResponse(BaseModel): - id: UUID + id: AnyStr name: str diff --git a/kontor-api/src/templates/admin/metadata.html b/kontor-api/src/templates/admin/metadata.html new file mode 100644 index 0000000..ca93b0b --- /dev/null +++ b/kontor-api/src/templates/admin/metadata.html @@ -0,0 +1,21 @@ +{% extends "shared/base.html" %} + +{% block title %} + MetaData +{% endblock %} + +{% block content %} + {% with msg=msg %} + {% include "components/alerts.html" %} + {% endwith %} +
+ {% for table in data %} +
+ {% with obj=table %} + {% include "components/metadatatable_cards.html" %} + {% endwith %} +
+
+ {% endfor %} +
+{% endblock %} diff --git a/kontor-api/src/templates/auth/login.html b/kontor-api/src/templates/auth/login.html new file mode 100644 index 0000000..0b2a94f --- /dev/null +++ b/kontor-api/src/templates/auth/login.html @@ -0,0 +1,40 @@ +{% extends "shared/base.html" %} + + +{% block title %} + Login +{% endblock %} + +{% block content %} +
+
+
Login to Kontor
+
+ {% for error in errors %} +
  • {{error}}
  • + {% endfor %} +
    +
    + {% if msg %} +
    + {{msg}} +
    + {% endif %} +
    +
    + +
    +
    +
    + + +
    +
    + + +
    + +
    +
    +
    + {% endblock %} diff --git a/kontor-api/src/templates/components/metadatatable_cards.html b/kontor-api/src/templates/components/metadatatable_cards.html new file mode 100644 index 0000000..ca19dbc --- /dev/null +++ b/kontor-api/src/templates/components/metadatatable_cards.html @@ -0,0 +1,35 @@ +
    +
    +
    {{obj.table_name}}
    + + + + + + + + + + + + + + + {% for column in obj.table_columns %} + + + + + + + + + + + + + {% endfor %} + +
    Column NameColumn Sync NameColumn TypeColumn ModifierColumn OrderIs ShownColumn LabelShow FilterFilter LabelRef Column
    {{column.column_name}}{{column.column_sync_name}}{{column.column_type}}{{column.column_modifier}}{{column.column_order}}{% with check=column.is_shown %}{% include "components/check.html" %}{% endwith %}{{column.column_label}}{% with check=column.show_filter %}{% include "components/check.html" %}{% endwith %}{{column.filter_label}}{{column.ref_column}}
    +
    +
    diff --git a/kontor-api/src/templates/components/navbar.html b/kontor-api/src/templates/components/navbar.html index 3d4f974..12c5e7a 100644 --- a/kontor-api/src/templates/components/navbar.html +++ b/kontor-api/src/templates/components/navbar.html @@ -42,7 +42,7 @@
  • Signup
  • Login
  • -
  • Something else here
  • +
  • MetaData
  • diff --git a/kontor-api/src/webapps/admin/__init__.py b/kontor-api/src/webapps/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/src/webapps/admin/route_admin.py b/kontor-api/src/webapps/admin/route_admin.py new file mode 100644 index 0000000..ffc6b78 --- /dev/null +++ b/kontor-api/src/webapps/admin/route_admin.py @@ -0,0 +1,24 @@ +from fastapi import APIRouter, Request +from fastapi.security.utils import get_authorization_scheme_param +from fastapi.templating import Jinja2Templates + +from src.apis.utils import SessionDep +from src.apis.version1.admin import get_current_user_from_token +from src.db.models.admin import Profile +from src.db.models.metadata import MetaDataColumn, MetaDataTable +from src.db.repository.metadata import get_columns + +templates = Jinja2Templates(directory="src/templates") +router = APIRouter(include_in_schema=False, prefix="/admin") + +@router.get("/metadata") +def get_metadata(db: SessionDep, request: Request, msg: str = None): + token = request.cookies.get("access_token") + scheme, param = get_authorization_scheme_param(token) + current_user: Profile = get_current_user_from_token(token=param, db=db) + data = [] + if current_user is None: + msg = "You are not logged in" + return templates.TemplateResponse("admin/metadata.html", {"request": request, "msg": msg, "data": data}) + tables= db.query(MetaDataTable).all() + return templates.TemplateResponse("admin/metadata.html", {"request": request, "msg": msg, "data": tables}) diff --git a/kontor-api/src/webapps/auth/__init__.py b/kontor-api/src/webapps/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/src/webapps/auth/forms.py b/kontor-api/src/webapps/auth/forms.py new file mode 100644 index 0000000..29a5aa7 --- /dev/null +++ b/kontor-api/src/webapps/auth/forms.py @@ -0,0 +1,27 @@ +from typing import List +from typing import Optional + +from fastapi import Request + + +class LoginForm: + def __init__(self, request: Request): + self.request: Request = request + self.errors: List = [] + self.username: Optional[str] = None + self.password: Optional[str] = None + + async def load_data(self): + form = await self.request.form() + # since auth works on username field we are considering email as username + self.username = form.get("email") + self.password = form.get("password") + + async def is_valid(self): + if not self.username or not (self.username.__contains__("@")): + self.errors.append("Email is required") + if not self.password or not len(self.password) >= 4: + self.errors.append("A valid password is required") + if not self.errors: + return True + return False diff --git a/kontor-api/src/webapps/auth/route_login.py b/kontor-api/src/webapps/auth/route_login.py new file mode 100644 index 0000000..737cec7 --- /dev/null +++ b/kontor-api/src/webapps/auth/route_login.py @@ -0,0 +1,35 @@ +from src.apis.version1.admin import login_for_access_token +from src.db.session import get_db +from fastapi import APIRouter +from fastapi import Depends +from fastapi import HTTPException +from fastapi import Request +from fastapi.templating import Jinja2Templates +from sqlalchemy.orm import Session +from src.webapps.auth.forms import LoginForm + + +templates = Jinja2Templates(directory="src/templates") +router = APIRouter(include_in_schema=False) + + +@router.get("/login/") +def login(request: Request): + return templates.TemplateResponse("auth/login.html", {"request": request}) + + +@router.post("/login/") +async def login(request: Request, db: Session = Depends(get_db)): + form = LoginForm(request) + await form.load_data() + if await form.is_valid(): + try: + form.__dict__.update(msg="Login Successful :)") + response = templates.TemplateResponse("auth/login.html", form.__dict__) + login_for_access_token(response=response, form_data=form, db=db) + return response + except HTTPException: + form.__dict__.update(msg="") + form.__dict__.get("errors").append("Incorrect Email or Password") + return templates.TemplateResponse("auth/login.html", form.__dict__) + return templates.TemplateResponse("auth/login.html", form.__dict__) \ No newline at end of file diff --git a/kontor-api/src/webapps/base.py b/kontor-api/src/webapps/base.py index 24b16cf..bad23ba 100644 --- a/kontor-api/src/webapps/base.py +++ b/kontor-api/src/webapps/base.py @@ -1,6 +1,8 @@ from fastapi import APIRouter, Request from fastapi.templating import Jinja2Templates +from src.webapps.admin import route_admin +from src.webapps.auth import route_login from src.webapps.comic import route_comics from src.webapps.media import route_media @@ -9,6 +11,8 @@ templates = Jinja2Templates(directory="src/templates") api_router = APIRouter() api_router.include_router(route_comics.router) api_router.include_router(route_media.router) +api_router.include_router(route_login.router) +api_router.include_router(route_admin.router) @api_router.get("/") def home(request: Request, msg: str = None): diff --git a/kontor-api/src/webapps/media/route_media.py b/kontor-api/src/webapps/media/route_media.py index b957f49..50f0762 100644 --- a/kontor-api/src/webapps/media/route_media.py +++ b/kontor-api/src/webapps/media/route_media.py @@ -1,11 +1,13 @@ -from uuid import UUID +from typing import AnyStr from fastapi import APIRouter, Request +from fastapi.security.utils import get_authorization_scheme_param from fastapi.templating import Jinja2Templates from src.apis.utils import SessionDep +from src.apis.version1.admin import get_current_user_from_token +from src.db.models.admin import Profile from src.db.models.media import MediaFile, MediaActor -#ifrom src.schema.media.comic import get_comic_details templates = Jinja2Templates(directory="src/templates") router = APIRouter(include_in_schema=False, prefix="/media") @@ -13,10 +15,19 @@ router = APIRouter(include_in_schema=False, prefix="/media") @router.get("/files") def get_mediafiles(db: SessionDep, request: Request, msg: str = None): mediafiles = db.query(MediaFile).all() - return templates.TemplateResponse("media/files.html", {"request": request, "msg": msg, "mediafiles": mediafiles}) + try: + token = request.cookies.get("access_token") + scheme, param = get_authorization_scheme_param(token) # scheme will hold "Bearer" and param will hold actual token value + current_user: Profile = get_current_user_from_token(token=param, db=db) + return templates.TemplateResponse("media/files.html", + {"request": request, "msg": msg, "mediafiles": mediafiles}) + except Exception as e: + print(e) + msg = "Nicht berechtigt!!" + return templates.TemplateResponse("media/files.html", {"request": request, "msg": msg, "mediafiles": []}) @router.get("/files/{file_id}") -def file_details(file_id: UUID, request: Request, db: SessionDep): +def file_details(file_id: AnyStr, request: Request, db: SessionDep): mediafile = db.get(MediaFile, file_id) return templates.TemplateResponse("media/file_detail.html", {"request": request, "mediafile":mediafile}) @@ -26,7 +37,7 @@ def get_actors(db: SessionDep, request: Request, msg: str = None): return templates.TemplateResponse("media/actors.html", {"request": request, "msg": msg, "actors": actors}) @router.get("/actors/{actor_id}") -def artist_detail(actor_id: UUID, request: Request, db: SessionDep): +def artist_detail(actor_id: AnyStr, request: Request, db: SessionDep): actor = db.get(MediaActor, actor_id) return templates.TemplateResponse("media/actor_detail.html", {"request": request, "actor": actor}) diff --git a/kontor-api/uv.lock b/kontor-api/uv.lock index 2383d42..f35d9ad 100644 --- a/kontor-api/uv.lock +++ b/kontor-api/uv.lock @@ -2,6 +2,18 @@ version = 1 revision = 2 requires-python = ">=3.13" +[[package]] +name = "aiosqlite" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload_time = "2025-02-03T07:30:16.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload_time = "2025-02-03T07:30:13.6Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -24,6 +36,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload_time = "2025-03-17T00:02:52.713Z" }, ] +[[package]] +name = "asyncpg" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746, upload_time = "2024-10-20T00:30:41.127Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373, upload_time = "2024-10-20T00:29:55.165Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745, upload_time = "2024-10-20T00:29:57.14Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103, upload_time = "2024-10-20T00:29:58.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471, upload_time = "2024-10-20T00:30:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253, upload_time = "2024-10-20T00:30:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720, upload_time = "2024-10-20T00:30:04.501Z" }, + { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404, upload_time = "2024-10-20T00:30:06.537Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623, upload_time = "2024-10-20T00:30:09.024Z" }, +] + +[[package]] +name = "bcrypt" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload_time = "2025-02-28T01:24:09.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload_time = "2025-02-28T01:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload_time = "2025-02-28T01:22:38.078Z" }, + { url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload_time = "2025-02-28T01:22:40.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload_time = "2025-02-28T01:22:43.144Z" }, + { url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload_time = "2025-02-28T01:22:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload_time = "2025-02-28T01:22:47.023Z" }, + { url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload_time = "2025-02-28T01:22:49.221Z" }, + { url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload_time = "2025-02-28T01:22:51.603Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload_time = "2025-02-28T01:22:53.283Z" }, + { url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload_time = "2025-02-28T01:22:55.461Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload_time = "2025-02-28T01:22:57.81Z" }, + { url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload_time = "2025-02-28T01:22:59.181Z" }, + { url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload_time = "2025-02-28T01:23:00.763Z" }, + { url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload_time = "2025-02-28T01:23:02.908Z" }, + { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload_time = "2025-02-28T01:23:05.838Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload_time = "2025-02-28T01:23:07.274Z" }, + { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload_time = "2025-02-28T01:23:09.151Z" }, + { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload_time = "2025-02-28T01:23:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload_time = "2025-02-28T01:23:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload_time = "2025-02-28T01:23:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload_time = "2025-02-28T01:23:16.686Z" }, + { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload_time = "2025-02-28T01:23:18.897Z" }, + { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload_time = "2025-02-28T01:23:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload_time = "2025-02-28T01:23:23.183Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload_time = "2025-02-28T01:23:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload_time = "2025-02-28T01:23:26.875Z" }, + { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload_time = "2025-02-28T01:23:28.381Z" }, + { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload_time = "2025-02-28T01:23:30.187Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload_time = "2025-02-28T01:23:31.945Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload_time = "2025-02-28T01:23:34.161Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload_time = "2025-02-28T01:23:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload_time = "2025-02-28T01:23:38.021Z" }, + { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload_time = "2025-02-28T01:23:39.575Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload_time = "2025-02-28T01:23:40.901Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload_time = "2025-02-28T01:23:42.653Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload_time = "2025-02-28T01:23:43.964Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload_time = "2025-02-28T01:23:46.011Z" }, + { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload_time = "2025-02-28T01:23:47.575Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload_time = "2025-02-28T01:23:49.059Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload_time = "2025-02-28T01:23:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload_time = "2025-02-28T01:23:51.775Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload_time = "2025-02-28T01:23:53.139Z" }, +] + [[package]] name = "beautifulsoup4" version = "4.13.4" @@ -118,6 +196,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload_time = "2025-03-30T20:36:43.61Z" }, ] +[[package]] +name = "databases" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/de/ea55722907bd1b2389b01e362faa3c66a09d5a8463c13d44c80da7b32497/databases-0.9.0.tar.gz", hash = "sha256:d2f259677609bf187737644c95fa41701072e995dfeb8d2882f335795c5b61b0", size = 31084, upload_time = "2024-03-01T17:39:28.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/43/6035922af5471f1a196851831a1d5f403447401b395f474bf673efa8959f/databases-0.9.0-py3-none-any.whl", hash = "sha256:9ee657c9863b34f8d3a06c06eafbe1bda68af2a434b56996312edf1f1c0b6297", size = 25580, upload_time = "2024-03-01T17:39:26.352Z" }, +] + +[package.optional-dependencies] +sqlite = [ + { name = "aiosqlite" }, +] + [[package]] name = "dnspython" version = "2.7.0" @@ -309,14 +404,19 @@ name = "kontor-api" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "asyncpg" }, + { name = "bcrypt" }, { name = "beautifulsoup4" }, + { name = "databases", extra = ["sqlite"] }, { name = "fastapi", extra = ["standard"] }, { name = "httpx" }, + { name = "jinja2" }, { name = "mariadb" }, { name = "natsort" }, { name = "pathlib" }, { name = "platformdirs" }, { name = "psycopg2-binary" }, + { name = "pydantic", extra = ["email"] }, { name = "pytest" }, { name = "pytest-cov" }, { name = "python-dotenv" }, @@ -330,14 +430,19 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "asyncpg", specifier = ">=0.30.0" }, + { name = "bcrypt", specifier = ">=4.3.0" }, { name = "beautifulsoup4", specifier = ">=4.13.4" }, + { name = "databases", extras = ["sqlite"], specifier = ">=0.9.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" }, { name = "httpx", specifier = "==0.24.1" }, + { name = "jinja2", specifier = ">=3.1.6" }, { name = "mariadb", specifier = ">=1.1.12" }, { name = "natsort", specifier = ">=8.4.0" }, { name = "pathlib", specifier = ">=1.0.1" }, { name = "platformdirs", specifier = ">=4.3.7" }, { name = "psycopg2-binary", specifier = ">=2.9.10" }, + { name = "pydantic", extras = ["email"], specifier = ">=2.11.3" }, { name = "pytest", specifier = "==7.4.0" }, { name = "pytest-cov", specifier = ">=6.1.1" }, { name = "python-dotenv", specifier = ">=1.1.0" }, @@ -499,6 +604,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591, upload_time = "2025-04-08T13:27:03.789Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-core" version = "2.33.1"