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}}
+
+
+ | Column Name |
+ Column Sync Name |
+ Column Type |
+ Column Modifier |
+ Column Order |
+ Is Shown |
+ Column Label |
+ Show Filter |
+ Filter Label |
+ Ref Column |
+
+
+ {% for column in obj.table_columns %}
+
+ | {{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}} |
+
+ {% endfor %}
+
+
+
+
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"