add endpoints for kontor-api

This commit is contained in:
Thomas Peetz
2026-01-09 17:04:46 +01:00
parent 98eb72bd22
commit e5d4b748dc
12 changed files with 95 additions and 30 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
from datetime import timedelta from datetime import timedelta
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, HTTPException, status, Depends, Response from fastapi import APIRouter, Body, HTTPException, status, Depends, Response
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from src.core.config import settings from src.core.config import settings
@@ -31,7 +31,7 @@ def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depen
# @router.post("/token-cookie", response_model=Token) # @router.post("/token-cookie", response_model=Token)
def login_for_token_cookie(response: Response, form_data: LoginForm = Depends()): def login_for_token_cookie(response: Response, form_data: LoginForm = Depends()):
user = authenticate_user(form_data.username, form_data.password) user = authenticate_user(form_data.username, form_data.password) # type: ignore
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
+37
View File
@@ -0,0 +1,37 @@
from datetime import timedelta
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from src.core.config import settings
from src.core.log_conf import logger
from src.core.security import authenticate_user, create_access_token
from src.schema.admin import Token
login_router = APIRouter()
class LoginRequest(BaseModel):
email: str | None = None
password: str | None = None
@login_router.post(
"/login",
tags=["login"],
summary="Login and get token",
response_description="Return HTTP status code 200 (OK)",
status_code=status.HTTP_200_OK
)
def login(request: LoginRequest) -> Token:
logger.info(f"login with {request.email} with {request.password}")
print(f"login with {request.email} with {request.password}")
user = authenticate_user(request.email, request.password) # type: ignore
scopes = ["admin", "read"]
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(data={"sub": user.email, "scope": " ".join(scopes)}, expires_delta=access_token_expires)
return Token(access_token=access_token, token_type="bearer")
+11 -4
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, status, HTTPException from fastapi import APIRouter, status, HTTPException
from sqlalchemy import select from sqlalchemy import select
from src.core.log_conf import logger from src.core.log_conf import logger
from src.db.repository.media import create_new_mediaactor from src.db.repository.media import create_new_mediaactor, delete_mediaactor
from src.db.session import SessionDep from src.db.session import SessionDep
from src.schema.media.actor import Actor, MediaActorResponse, get_actor_details from src.schema.media.actor import Actor, MediaActorResponse, get_actor_details
from src.db.models.media import MediaActor from src.db.models.media import MediaActor
@@ -10,7 +10,7 @@ router = APIRouter()
@router.get("/actors", response_model=list[MediaActorResponse]) @router.get("/actors", response_model=list[MediaActorResponse])
# def get_all_files(db: SessionDep, review: bool = False, download: bool = False, current_user: Profile = Depends(get_current_user_from_token)) -> 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]:
def get_all_actors(db: SessionDep, review: bool = False, download: bool = False) -> list[MediaActorResponse]: def get_all_actors(db: SessionDep, review: bool = False, download: bool = False) -> list[MediaActorResponse]: # type: ignore
results: list[MediaActorResponse] = [] results: list[MediaActorResponse] = []
actors = db.scalars(select(MediaActor)).all() actors = db.scalars(select(MediaActor)).all()
for mediaactor in actors: for mediaactor in actors:
@@ -19,15 +19,22 @@ def get_all_actors(db: SessionDep, review: bool = False, download: bool = False)
return results return results
@router.get("/actors/{actor_id}", response_model=MediaActorResponse) @router.get("/actors/{actor_id}", response_model=MediaActorResponse)
def get_actor(actor_id: str, db: SessionDep) -> MediaActorResponse: def get_actor(actor_id: str, db: SessionDep) -> MediaActorResponse: # type: ignore
media_actor = db.get(MediaActor, actor_id) media_actor = db.get(MediaActor, actor_id)
if not media_actor: if not media_actor:
raise HTTPException(status_code=404, detail="MediaActor could not be found") raise HTTPException(status_code=404, detail="MediaActor could not be found")
response = get_actor_details(media_actor) response = get_actor_details(media_actor)
return response return response
@router.delete("/actors/{actor_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_actor(actor_id: str, db: SessionDep): # type: ignore
media_actor = db.get(MediaActor, actor_id)
if not media_actor:
raise HTTPException(status_code=404, detail="MediaActor could not be found")
delete_mediaactor(db, media_actor.id)
@router.post("/actors", status_code=status.HTTP_201_CREATED) @router.post("/actors", status_code=status.HTTP_201_CREATED)
def add_actor(new_actor: Actor, db: SessionDep) -> MediaActorResponse: def add_actor(new_actor: Actor, db: SessionDep) -> MediaActorResponse: # type: ignore
logger.info(f"add actor {new_actor.url}") logger.info(f"add actor {new_actor.url}")
try: try:
mediaActor: MediaActor = create_new_mediaactor(new_actor, db) mediaActor: MediaActor = create_new_mediaactor(new_actor, db)
@@ -8,7 +8,7 @@ from src.schema.media.actorfile import MediaActorFileResponse, get_actorfile_det
router = APIRouter() router = APIRouter()
@router.get("/actorfiles", response_model=list[MediaActorFileResponse]) @router.get("/actorfiles", response_model=list[MediaActorFileResponse])
def get_all_actorfiles(db: SessionDep) -> list[MediaActorFileResponse]: def get_all_actorfiles(db: SessionDep) -> list[MediaActorFileResponse]: # type: ignore
results: list[MediaActorFileResponse] = [] results: list[MediaActorFileResponse] = []
actorfiles = db.scalars(select(MediaActorFile)).all() actorfiles = db.scalars(select(MediaActorFile)).all()
for mediaactorfile in actorfiles: for mediaactorfile in actorfiles:
@@ -17,7 +17,7 @@ def get_all_actorfiles(db: SessionDep) -> list[MediaActorFileResponse]:
return results return results
@router.get("/actorfiles/{actorfile_id}", response_model=MediaActorFileResponse) @router.get("/actorfiles/{actorfile_id}", response_model=MediaActorFileResponse)
def get_actorfile(actorfile_id: str, db: SessionDep) -> MediaActorFileResponse: def get_actorfile(actorfile_id: str, db: SessionDep) -> MediaActorFileResponse: # type: ignore
media_actorfile = db.get(MediaActorFile, actorfile_id) media_actorfile = db.get(MediaActorFile, actorfile_id)
if not media_actorfile: if not media_actorfile:
raise HTTPException(status_code=404, detail="MediaActor could not be found") raise HTTPException(status_code=404, detail="MediaActor could not be found")
@@ -25,7 +25,7 @@ def get_actorfile(actorfile_id: str, db: SessionDep) -> MediaActorFileResponse:
return response return response
@router.delete("/actorfiles/{actorfile_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/actorfiles/{actorfile_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_actorfile(actorfile_id: str, db: SessionDep): def delete_actorfile(actorfile_id: str, db: SessionDep): # type: ignore
media_actorfile = db.get(MediaActorFile, actorfile_id) media_actorfile = db.get(MediaActorFile, actorfile_id)
if not media_actorfile: if not media_actorfile:
raise HTTPException(status_code=404, detail="MediaActor could not be found") raise HTTPException(status_code=404, detail="MediaActor could not be found")
+1 -1
View File
@@ -14,7 +14,7 @@ class Settings:
DB_USER: str = os.getenv("DB_USER", "kontor") DB_USER: str = os.getenv("DB_USER", "kontor")
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "kontor") DB_PASSWORD: str = os.getenv("DB_PASSWORD", "kontor")
DB_SERVER: str = os.getenv("DB_SERVER", "postgres") DB_SERVER: str = os.getenv("DB_SERVER", "postgres")
DB_PORT: str = os.getenv("DB_PORT", 5432) DB_PORT: int = int(os.getenv("DB_PORT", 5432))
DB_DBNAME: str = os.getenv("DB_DBNAME", "kontor") DB_DBNAME: str = os.getenv("DB_DBNAME", "kontor")
DATABASE_URL: str = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_SERVER}:{DB_PORT}/{DB_DBNAME}" DATABASE_URL: str = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_SERVER}:{DB_PORT}/{DB_DBNAME}"
SECRET_KEY: str = os.getenv("SECRET_KEY", "J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc=") SECRET_KEY: str = os.getenv("SECRET_KEY", "J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc=")
+9 -9
View File
@@ -41,11 +41,11 @@ class OAuth2PasswordBearerWithCookie(OAuth2):
): ):
if not scopes: if not scopes:
scopes = {} scopes = {}
flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes}) flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes}) # type: ignore
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error) super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
async def __call__(self, request: Request) -> Optional[str]: async def __call__(self, request: Request) -> Optional[str]:
authorization: str = request.cookies.get("access_token") # changed to accept access token from httpOnly Cookie authorization: str = request.cookies.get("access_token") # type: ignore # changed to accept access token from httpOnly Cookie
scheme, param = get_authorization_scheme_param(authorization) scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer": if not authorization or scheme.lower() != "bearer":
@@ -100,7 +100,7 @@ async def get_current_user(security_scopes: SecurityScopes, token: Annotated[str
) )
try: try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
username: str = payload.get("sub") username: str = payload.get("sub") # type: ignore
logger.info("username/email extracted is ", username) logger.info("username/email extracted is ", username)
if username is None: if username is None:
raise credentials_exception raise credentials_exception
@@ -110,7 +110,7 @@ async def get_current_user(security_scopes: SecurityScopes, token: Annotated[str
except (JWTError, ValidationError): except (JWTError, ValidationError):
raise credentials_exception raise credentials_exception
with SessionLocal() as db: with SessionLocal() as db:
user = get_profile(username=token_data.username, db=db) user = get_profile(username=token_data.username, db=db) # type: ignore
if user is None: if user is None:
raise credentials_exception raise credentials_exception
for scope in security_scopes.scopes: for scope in security_scopes.scopes:
@@ -126,11 +126,11 @@ async def get_current_user(security_scopes: SecurityScopes, token: Annotated[str
async def get_current_active_user( async def get_current_active_user(
current_user: Annotated[Profile, Security(get_current_user, scopes=["me"])], current_user: Annotated[Profile, Security(get_current_user, scopes=["me"])],
) -> ProfileModel: ) -> ProfileModel:
if not current_user.enabled: if not current_user.enabled: # type: ignore
raise HTTPException(status_code=400, detail="Inactive user") raise HTTPException(status_code=400, detail="Inactive user")
user_model = ProfileModel(username=current_user.user_name, email=current_user.email, user_model = ProfileModel(username=current_user.user_name, email=current_user.email, # type: ignore
first_name=current_user.first_name, last_name=current_user.last_name, first_name=current_user.first_name, last_name=current_user.last_name, # type: ignore
active=current_user.enabled) active=current_user.enabled) # type: ignore
return user_model return user_model
@@ -144,7 +144,7 @@ def get_current_user_from_token(token: str = Depends(oauth2_scheme)):
payload = jwt.decode( payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM] token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
) )
username: str = payload.get("sub") username: str = payload.get("sub") # type: ignore
logger.info("username/email extracted is ", username) logger.info("username/email extracted is ", username)
if username is None: if username is None:
raise credentials_exception raise credentials_exception
+6
View File
@@ -59,6 +59,12 @@ def create_new_mediaactor(new_actor: Actor, db: Session) -> MediaActor:
logger.info(f"created {media_actor}") logger.info(f"created {media_actor}")
return media_actor return media_actor
def delete_mediaactor(db: Session, actor_id: str):
logger.info(f"delete MediaActor with id {actor_id}")
media_actor = db.get(MediaActor, actor_id)
db.delete(media_actor)
db.commit()
def create_new_mediaactorfile(db: Session, actor_id: str, file_id: str) -> MediaActorFile: def create_new_mediaactorfile(db: Session, actor_id: str, file_id: str) -> MediaActorFile:
logger.info(f"create MediaActorFile with actor {actor_id} and file {file_id}") logger.info(f"create MediaActorFile with actor {actor_id} and file {file_id}")
media_actor_file: MediaActorFile = MediaActorFile() media_actor_file: MediaActorFile = MediaActorFile()
+2
View File
@@ -6,6 +6,7 @@ from fastapi.staticfiles import StaticFiles
from src.apis.base import api_router from src.apis.base import api_router
from src.apis.version1.healthcheck import health_router from src.apis.version1.healthcheck import health_router
from src.apis.version1.login import login_router
from src.core.config import settings from src.core.config import settings
from src.core.log_conf import logger from src.core.log_conf import logger
from src.db.models.base import Base from src.db.models.base import Base
@@ -25,6 +26,7 @@ def include_router(app: FastAPI):
app.include_router(api_router) app.include_router(api_router)
app.include_router(web_app_router) app.include_router(web_app_router)
app.include_router(health_router) app.include_router(health_router)
app.include_router(login_router)
def configure_static(app: FastAPI): def configure_static(app: FastAPI):
+17 -5
View File
@@ -18,9 +18,21 @@ type jwtCustomClaims struct {
jwt.RegisteredClaims jwt.RegisteredClaims
} }
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
func Login(c echo.Context) error { func Login(c echo.Context) error {
user := c.FormValue("user") // user := c.FormValue("email")
pass := c.FormValue("pass") // pass := c.FormValue("password")
loginRequest := new(LoginRequest)
if err := c.Bind(loginRequest); err != nil {
return err
}
email := loginRequest.Email
password := loginRequest.Password
var profile schema.Profile var profile schema.Profile
var err error var err error
@@ -28,12 +40,12 @@ func Login(c echo.Context) error {
ctx := context.Background() ctx := context.Background()
db, _ = schema.GetDatabase() db, _ = schema.GetDatabase()
err = db.NewSelect().Model(&profile).Where("email = ?", user).Scan(ctx) err = db.NewSelect().Model(&profile).Where("email = ?", email).Scan(ctx)
if err != nil { if err != nil {
return c.String(http.StatusInternalServerError, err.Error()) return c.String(http.StatusInternalServerError, err.Error())
} }
if !utils.ComparePassword(profile.Password, pass) { if !utils.ComparePassword(profile.Password, password) {
return echo.ErrUnauthorized return echo.ErrUnauthorized
} }
@@ -55,7 +67,7 @@ func Login(c echo.Context) error {
return err return err
} }
return c.JSON(http.StatusOK, echo.Map{"token": t}) return c.JSON(http.StatusOK, echo.Map{"access_token": t, "token_type": "bearer"})
} }
func restricted(c echo.Context) error { func restricted(c echo.Context) error {
+7 -4
View File
@@ -75,17 +75,20 @@ def get_api_config(log: Logger, config: str) -> Dict[str, Any]:
log.info("Call login first") log.info("Call login first")
login_url = f"http://{host}:{port}/login" login_url = f"http://{host}:{port}/login"
login_data = {} login_data = {}
login_data['user'] = api_data["user"] login_data['email'] = api_data["email"]
login_data['pass'] = api_data["pass"] login_data['password'] = api_data["password"]
response = requests.post(login_url, data=login_data) response = requests.post(login_url, json=login_data)
status = response.status_code status = response.status_code
log.info(f"Status: {status}") log.info(f"Status: {status}")
if status != 200: if status != 200:
log.fatal("authentication failed") log.fatal("authentication failed")
return api_data return api_data
data = response.json() data = response.json()
token = data['token'] log.debug(f"got data: {data}")
token = data['access_token']
token_type = data['token_type']
api_data['token'] = token api_data['token'] = token
api_data['token_type'] = token_type
with open(api_config, 'w') as f: with open(api_config, 'w') as f:
yaml.dump(api_data, f) yaml.dump(api_data, f)
else: else:
-1
View File
@@ -169,5 +169,4 @@ if __name__ == '__main__':
item_delete(table_name=tablename, item_id=item_id, api_data=api_data, log=logger) item_delete(table_name=tablename, item_id=item_id, api_data=api_data, log=logger)
case _: case _:
logger.info("Method to remove remaining item not implemented") logger.info("Method to remove remaining item not implemented")
logger.info('kontor.import finished') logger.info('kontor.import finished')
@@ -13,7 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;