From e5d4b748dc0cd21f73f1476d99c00027214c910f Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Fri, 9 Jan 2026 17:04:46 +0100 Subject: [PATCH] add endpoints for kontor-api --- kontor-api/src/apis/version1/admin.py | 4 +- kontor-api/src/apis/version1/login.py | 37 +++++++++++++++++++ kontor-api/src/apis/version1/mediaactor.py | 15 ++++++-- .../src/apis/version1/mediaactorfile.py | 6 +-- kontor-api/src/core/config.py | 2 +- kontor-api/src/core/security.py | 18 ++++----- kontor-api/src/db/repository/media.py | 6 +++ kontor-api/src/main.py | 2 + kontor-echo/pkg/handler/auth.go | 22 ++++++++--- kontor-scripts/config.py | 11 ++++-- kontor-scripts/import.py | 1 - .../kontor/security/SecurityConfig.java | 1 - 12 files changed, 95 insertions(+), 30 deletions(-) create mode 100644 kontor-api/src/apis/version1/login.py diff --git a/kontor-api/src/apis/version1/admin.py b/kontor-api/src/apis/version1/admin.py index ccd596c..6214a45 100644 --- a/kontor-api/src/apis/version1/admin.py +++ b/kontor-api/src/apis/version1/admin.py @@ -1,7 +1,7 @@ from datetime import timedelta 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 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) 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: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/kontor-api/src/apis/version1/login.py b/kontor-api/src/apis/version1/login.py new file mode 100644 index 0000000..73a0d2e --- /dev/null +++ b/kontor-api/src/apis/version1/login.py @@ -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") diff --git a/kontor-api/src/apis/version1/mediaactor.py b/kontor-api/src/apis/version1/mediaactor.py index d70baa5..20aa4dc 100644 --- a/kontor-api/src/apis/version1/mediaactor.py +++ b/kontor-api/src/apis/version1/mediaactor.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, status, HTTPException from sqlalchemy import select 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.schema.media.actor import Actor, MediaActorResponse, get_actor_details from src.db.models.media import MediaActor @@ -10,7 +10,7 @@ router = APIRouter() @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_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] = [] actors = db.scalars(select(MediaActor)).all() for mediaactor in actors: @@ -19,15 +19,22 @@ def get_all_actors(db: SessionDep, review: bool = False, download: bool = False) return results @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) if not media_actor: raise HTTPException(status_code=404, detail="MediaActor could not be found") response = get_actor_details(media_actor) 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) -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}") try: mediaActor: MediaActor = create_new_mediaactor(new_actor, db) diff --git a/kontor-api/src/apis/version1/mediaactorfile.py b/kontor-api/src/apis/version1/mediaactorfile.py index 0c0d657..2d9524c 100644 --- a/kontor-api/src/apis/version1/mediaactorfile.py +++ b/kontor-api/src/apis/version1/mediaactorfile.py @@ -8,7 +8,7 @@ from src.schema.media.actorfile import MediaActorFileResponse, get_actorfile_det router = APIRouter() @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] = [] actorfiles = db.scalars(select(MediaActorFile)).all() for mediaactorfile in actorfiles: @@ -17,7 +17,7 @@ def get_all_actorfiles(db: SessionDep) -> list[MediaActorFileResponse]: return results @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) if not media_actorfile: 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 @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) if not media_actorfile: raise HTTPException(status_code=404, detail="MediaActor could not be found") diff --git a/kontor-api/src/core/config.py b/kontor-api/src/core/config.py index 47f52ba..135454f 100644 --- a/kontor-api/src/core/config.py +++ b/kontor-api/src/core/config.py @@ -14,7 +14,7 @@ class Settings: DB_USER: str = os.getenv("DB_USER", "kontor") DB_PASSWORD: str = os.getenv("DB_PASSWORD", "kontor") 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") DATABASE_URL: str = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_SERVER}:{DB_PORT}/{DB_DBNAME}" SECRET_KEY: str = os.getenv("SECRET_KEY", "J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc=") diff --git a/kontor-api/src/core/security.py b/kontor-api/src/core/security.py index 5ed2b2d..ad3e42e 100644 --- a/kontor-api/src/core/security.py +++ b/kontor-api/src/core/security.py @@ -41,11 +41,11 @@ class OAuth2PasswordBearerWithCookie(OAuth2): ): if not 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) 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) if not authorization or scheme.lower() != "bearer": @@ -100,7 +100,7 @@ async def get_current_user(security_scopes: SecurityScopes, token: Annotated[str ) try: 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) if username is None: raise credentials_exception @@ -110,7 +110,7 @@ async def get_current_user(security_scopes: SecurityScopes, token: Annotated[str except (JWTError, ValidationError): raise credentials_exception 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: raise credentials_exception 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( current_user: Annotated[Profile, Security(get_current_user, scopes=["me"])], ) -> ProfileModel: - if not current_user.enabled: + if not current_user.enabled: # type: ignore raise HTTPException(status_code=400, detail="Inactive user") - user_model = ProfileModel(username=current_user.user_name, email=current_user.email, - first_name=current_user.first_name, last_name=current_user.last_name, - active=current_user.enabled) + user_model = ProfileModel(username=current_user.user_name, email=current_user.email, # type: ignore + first_name=current_user.first_name, last_name=current_user.last_name, # type: ignore + active=current_user.enabled) # type: ignore return user_model @@ -144,7 +144,7 @@ def get_current_user_from_token(token: str = Depends(oauth2_scheme)): payload = jwt.decode( token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM] ) - username: str = payload.get("sub") + username: str = payload.get("sub") # type: ignore logger.info("username/email extracted is ", username) if username is None: raise credentials_exception diff --git a/kontor-api/src/db/repository/media.py b/kontor-api/src/db/repository/media.py index 1bd044c..e7f3acd 100644 --- a/kontor-api/src/db/repository/media.py +++ b/kontor-api/src/db/repository/media.py @@ -59,6 +59,12 @@ def create_new_mediaactor(new_actor: Actor, db: Session) -> MediaActor: logger.info(f"created {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: logger.info(f"create MediaActorFile with actor {actor_id} and file {file_id}") media_actor_file: MediaActorFile = MediaActorFile() diff --git a/kontor-api/src/main.py b/kontor-api/src/main.py index 46dcc4e..b0301f2 100644 --- a/kontor-api/src/main.py +++ b/kontor-api/src/main.py @@ -6,6 +6,7 @@ from fastapi.staticfiles import StaticFiles from src.apis.base import api_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.log_conf import logger from src.db.models.base import Base @@ -25,6 +26,7 @@ def include_router(app: FastAPI): app.include_router(api_router) app.include_router(web_app_router) app.include_router(health_router) + app.include_router(login_router) def configure_static(app: FastAPI): diff --git a/kontor-echo/pkg/handler/auth.go b/kontor-echo/pkg/handler/auth.go index dce89bb..3b1dd0c 100644 --- a/kontor-echo/pkg/handler/auth.go +++ b/kontor-echo/pkg/handler/auth.go @@ -18,9 +18,21 @@ type jwtCustomClaims struct { jwt.RegisteredClaims } +type LoginRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + func Login(c echo.Context) error { - user := c.FormValue("user") - pass := c.FormValue("pass") + // user := c.FormValue("email") + // 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 err error @@ -28,12 +40,12 @@ func Login(c echo.Context) error { ctx := context.Background() 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 { return c.String(http.StatusInternalServerError, err.Error()) } - if !utils.ComparePassword(profile.Password, pass) { + if !utils.ComparePassword(profile.Password, password) { return echo.ErrUnauthorized } @@ -55,7 +67,7 @@ func Login(c echo.Context) error { 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 { diff --git a/kontor-scripts/config.py b/kontor-scripts/config.py index c5d05a1..afad375 100644 --- a/kontor-scripts/config.py +++ b/kontor-scripts/config.py @@ -75,17 +75,20 @@ def get_api_config(log: Logger, config: str) -> Dict[str, Any]: log.info("Call login first") login_url = f"http://{host}:{port}/login" login_data = {} - login_data['user'] = api_data["user"] - login_data['pass'] = api_data["pass"] - response = requests.post(login_url, data=login_data) + login_data['email'] = api_data["email"] + login_data['password'] = api_data["password"] + response = requests.post(login_url, json=login_data) status = response.status_code log.info(f"Status: {status}") if status != 200: log.fatal("authentication failed") return api_data 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_type'] = token_type with open(api_config, 'w') as f: yaml.dump(api_data, f) else: diff --git a/kontor-scripts/import.py b/kontor-scripts/import.py index a566f02..b6abd73 100644 --- a/kontor-scripts/import.py +++ b/kontor-scripts/import.py @@ -169,5 +169,4 @@ if __name__ == '__main__': item_delete(table_name=tablename, item_id=item_id, api_data=api_data, log=logger) case _: logger.info("Method to remove remaining item not implemented") - logger.info('kontor.import finished') diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java b/kontor-spring/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java index 08b029f..0bb55a2 100644 --- a/kontor-spring/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java @@ -13,7 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;