Vorbereitung Release 0.2.0 #83
@@ -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,
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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=")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user