Vorbereitung Release 0.2.0 #83

Merged
tpeetz merged 178 commits from develop/0.2.0 into main 2026-01-29 22:50:42 +00:00
12 changed files with 95 additions and 30 deletions
Showing only changes of commit e5d4b748dc - Show all commits
+2 -2
View File
@@ -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,
+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 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)
@@ -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")
+1 -1
View File
@@ -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=")
+9 -9
View File
@@ -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
+6
View File
@@ -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()
+2
View File
@@ -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):
+17 -5
View File
@@ -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 {
+7 -4
View File
@@ -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:
-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)
case _:
logger.info("Method to remove remaining item not implemented")
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.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;