Strankovani

This commit is contained in:
Michal Kváček
2023-12-12 23:41:44 +01:00
parent f535d2866c
commit f4d709d621
15 changed files with 154 additions and 85 deletions
-1
View File
@@ -3,7 +3,6 @@ import os
APP_DEBUG = True
GRAPHIQL = True
ACCESS_TOKEN_VALIDITY_MINUTES = 20
REFRESH_TOKEN_VALIDITY_DAYS = 30
API_URL = os.environ.get("API_URL") or "http://localhost:8000"
+12 -2
View File
@@ -3,8 +3,18 @@ from fastapi import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
def public_endpoint(func):
pass
def allow_public(func):
@wraps(func)
async def decorator(*args, **kwargs):
if 'info' in kwargs:
user_id = kwargs['info'].context.user_id
public = kwargs.get('public')
if not user_id and not public:
raise HTTPException(HTTP_401_UNAUTHORIZED)
return await func(*args, **kwargs)
return decorator
def authenticated_user_only(raise_when_unauthorized: bool = True, return_value_unauthorized=None):
+1 -2
View File
@@ -41,8 +41,7 @@ class LoginEndpoint(AuthEndpoint):
return {
"user": user,
"access_token": access_token,
"access_token_validity": self.access_security.access_expires_delta.total_seconds(),
"access_token": access_token
}
@@ -21,4 +21,4 @@ flight_dataloader = create_dataloader(models.Flight)
photo_adjustment_dataloader = create_dataloader(
models.PhotoAdjustment, relationship_column=models.PhotoAdjustment.photo_id
)
photo_dataloader = create_dataloader(models.Photo)
photo_dataloader = create_dataloader(models.Photo)
+17 -11
View File
@@ -1,10 +1,8 @@
from typing import List, Optional
from typing import Optional
import strawberry
from fastapi import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
from decorators.endpoints import authenticated_user_only
from decorators.endpoints import authenticated_user_only, allow_public
from decorators.error_logging import error_logging
from .helpers.pagination import get_pagination_window, PaginationWindow
from .resolvers.aircraft import AircraftMutationResolver, AircraftQueryResolver
from graphql_schema.entities.types.mutation_input import CreateAircraftInput, EditAircraftInput
from graphql_schema.entities.types.types import Aircraft
@@ -15,20 +13,28 @@ class AircraftQueries:
@strawberry.field()
@error_logging
@authenticated_user_only()
async def aircrafts(root, info) -> List[Aircraft]:
return await AircraftQueryResolver().get_list(
async def aircrafts(root, info, limit: int, offset: int = 0) -> PaginationWindow[Aircraft]:
query = AircraftQueryResolver().get_query(
info.context.user_id,
organization_ids=info.context.organization_ids
)
return await get_pagination_window(
query=query,
item_type=Aircraft,
limit=limit,
offset=offset
)
@strawberry.field()
@error_logging
@allow_public
async def aircraft(root, info, id: int, public: Optional[bool] = False) -> Aircraft:
if not info.context.user_id and not public:
raise HTTPException(HTTP_401_UNAUTHORIZED)
return await AircraftQueryResolver().get_one(
id, user_id=info.context.user_id, organization_ids=info.context.organization_ids, public=public
id,
user_id=info.context.user_id,
organization_ids=info.context.organization_ids,
public=public
)
+2 -6
View File
@@ -1,11 +1,9 @@
from typing import List, Optional
import strawberry
from fastapi import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
from strawberry.types import Info
from database import models
from decorators.error_logging import error_logging
from decorators.endpoints import authenticated_user_only
from decorators.endpoints import authenticated_user_only, allow_public
from graphql_schema.entities.resolvers.base import BaseMutationResolver
from graphql_schema.entities.resolvers.copilot import CopilotQueryResolver
from graphql_schema.entities.types.mutation_input import CreateCopilotInput, EditCopilotInput
@@ -22,10 +20,8 @@ class CopilotQueries:
@strawberry.field()
@error_logging
@allow_public
async def copilot(root, info: Info, id: int, pilot_username: Optional[str] = None) -> Copilot:
if not info.context.user_id and not pilot_username:
raise HTTPException(HTTP_401_UNAUTHORIZED)
return await CopilotQueryResolver().get_one(
id,
user_id=info.context.user_id,
+24 -13
View File
@@ -1,10 +1,9 @@
from typing import List, Optional
import strawberry
from fastapi import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
from database import models
from decorators.endpoints import authenticated_user_only
from decorators.endpoints import authenticated_user_only, allow_public
from decorators.error_logging import error_logging
from graphql_schema.entities.helpers.pagination import PaginationWindow, get_pagination_window
from graphql_schema.entities.resolvers.base import BaseMutationResolver
from graphql_schema.entities.resolvers.event import EventQueryResolver
from graphql_schema.entities.types.mutation_input import CreateEventInput, EditEventInput
@@ -15,25 +14,37 @@ from graphql_schema.entities.types.types import Event
class EventQueries:
@strawberry.field()
@error_logging
async def events(root, info, username: Optional[str] = None) -> List[Event]:
if not info.context.user_id and not username:
raise HTTPException(HTTP_401_UNAUTHORIZED)
return await EventQueryResolver().get_list(
@allow_public
async def events(
root,
info,
limit: int,
offset: int = 0,
username: Optional[str] = None,
public: Optional[bool] = False,
) -> PaginationWindow[Event]:
query = EventQueryResolver().get_query(
info.context.user_id,
username=username,
order_by=[models.Event.date_from.desc(), models.Event.id.desc()]
order_by=[models.Event.date_from.desc(), models.Event.id.desc()],
public=public
)
return await get_pagination_window(
query=query,
item_type=Event,
limit=limit,
offset=offset
)
@strawberry.field()
@error_logging
async def event(root, info, id: int, username: Optional[str] = None) -> Event:
if not info.context.user_id and not username:
raise HTTPException(HTTP_401_UNAUTHORIZED)
@allow_public
async def event(root, info, id: int, username: Optional[str] = None, public: Optional[bool] = False) -> Event:
return await EventQueryResolver().get_one(
id,
username=username,
public=public,
user_id=info.context.user_id
)
+23 -15
View File
@@ -1,17 +1,16 @@
import asyncio
from typing import List, Optional
from typing import Optional
import strawberry
from fastapi import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
from background_jobs.weather import download_weather
from database import models
from decorators.endpoints import authenticated_user_only
from decorators.error_logging import error_logging
from database.transaction import get_session
from decorators.endpoints import authenticated_user_only, allow_public
from decorators.error_logging import error_logging
from graphql_schema.entities.resolvers.flight import handle_aircraft_save, FlightMutationResolver, FlightQueryResolver
from graphql_schema.entities.types.mutation_input import EditFlightInput, CreateFlightInput
from .helpers.combobox import handle_combobox_save
from graphql_schema.entities.types.types import Flight
from .helpers.combobox import handle_combobox_save
from .helpers.pagination import PaginationWindow, get_pagination_window
@strawberry.type
@@ -19,34 +18,43 @@ class FlightQueries:
@strawberry.field()
@error_logging
@allow_public
async def flights(
root, info,
limit: int,
offset: int = 0,
username: Optional[str] = None,
public: Optional[bool] = False,
copilot_id: Optional[int] = None,
point_of_interest_id: Optional[int] = None,
aircraft_id: Optional[int] = None,
) -> List[Flight]:
if not info.context.user_id and not public:
raise HTTPException(HTTP_401_UNAUTHORIZED)
return await FlightQueryResolver().get_list(
) -> PaginationWindow[Flight]:
query = FlightQueryResolver().get_query(
user_id=info.context.user_id,
username=username,
only_public=public,
copilot_id=copilot_id,
aircraft_id=aircraft_id,
point_of_interest_id=point_of_interest_id
)
return await get_pagination_window(
query=query,
item_type=Flight,
limit=limit,
offset=offset,
)
@strawberry.field()
@error_logging
@allow_public
async def flight(root, info, id: int, username: Optional[str] = None, public: Optional[bool] = False) -> Flight:
if not info.context.user_id and not public:
raise HTTPException(HTTP_401_UNAUTHORIZED)
return await FlightQueryResolver().get_one(id, user_id=info.context.user_id, username=username, public=public)
return await FlightQueryResolver().get_one(
id,
user_id=info.context.user_id,
username=username,
public=public
)
@strawberry.type
@@ -0,0 +1,43 @@
from typing import TypeVar, Generic, List
import strawberry
from sqlalchemy import func, Select
from database.transaction import get_session
Item = TypeVar("Item")
@strawberry.type
class PaginationWindow(Generic[Item]):
items: List[Item] = strawberry.field(
description="The list of items in this pagination window."
)
total_items_count: int = strawberry.field(
description="Total number of items in the filtered dataset."
)
async def get_pagination_window(
query: Select,
item_type: type,
limit: int,
offset: int = 0,
) -> PaginationWindow:
if limit <= 0:
raise Exception(f"limit ({limit}) must be > 0")
async with get_session() as db:
cnt_query = query.with_only_columns(func.count())
total_items_count = (await db.scalars(cnt_query)).one()
# if offset != 0 and not 0 <= offset < total_items_count:
# raise Exception(f"offset ({offset}) is out of range " f"(0-{total_items_count - 1})")
async with get_session() as db:
data = (await db.scalars(query.limit(limit).offset(offset))).all()
dataset = [item_type(**i.as_dict()) for i in data]
return PaginationWindow(
items=dataset,
total_items_count=total_items_count
)
+5 -3
View File
@@ -1,7 +1,7 @@
from typing import List, Optional
import strawberry
from database import models
from decorators.endpoints import authenticated_user_only
from decorators.endpoints import authenticated_user_only, allow_public
from decorators.error_logging import error_logging
from graphql_schema.entities.resolvers.base import BaseQueryResolver
from graphql_schema.entities.resolvers.photo import PhotoMutationResolver, PhotoQueryResolver
@@ -13,6 +13,7 @@ from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadP
class PhotoQueries:
@strawberry.field()
@error_logging
@allow_public
async def photos(
root, info,
flight_id: Optional[int] = None,
@@ -32,8 +33,9 @@ class PhotoQueries:
@strawberry.field()
@error_logging
async def photo(root, info, id: int) -> Photo:
return await BaseQueryResolver(Photo, models.Photo).get_one(id, user_id=info.context.user_id)
@allow_public
async def photo(root, info, id: int, public: Optional[bool] = False,) -> Photo:
return await BaseQueryResolver(Photo, models.Photo).get_one(id, user_id=info.context.user_id, public=public)
@strawberry.type
+17 -13
View File
@@ -1,12 +1,11 @@
from typing import List
import strawberry
from fastapi import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
from database import models
from decorators.endpoints import authenticated_user_only
from decorators.endpoints import authenticated_user_only, allow_public
from database.transaction import get_session
from decorators.error_logging import error_logging
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.helpers.pagination import get_pagination_window, PaginationWindow
from graphql_schema.entities.resolvers.base import BaseQueryResolver, BaseMutationResolver
from graphql_schema.entities.types.types import PointOfInterest
from graphql_schema.entities.types.mutation_input import CreatePointOfInterestInput, EditPointOfInterestInput
@@ -16,20 +15,25 @@ from graphql_schema.entities.types.mutation_input import CreatePointOfInterestIn
class PointOfInterestQueries:
@strawberry.field()
@error_logging
async def points_of_interest(root, info, public: bool = False) -> List[PointOfInterest]:
if not info.context.user_id and not public:
raise HTTPException(HTTP_401_UNAUTHORIZED)
return await BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_list(
info.context.user_id,
only_public=public
@allow_public
async def points_of_interest(
root, info,
limit: int, offset: int = 0,
public: bool = False
) -> PaginationWindow[PointOfInterest]:
query = BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_query(
info.context.user_id, only_public=public
)
return await get_pagination_window(
query=query,
item_type=PointOfInterest,
limit=limit,
offset=offset
)
@strawberry.field()
@allow_public
async def point_of_interest(root, info, id: int, public: bool = False) -> PointOfInterest:
if not info.context.user_id and not public:
raise HTTPException(HTTP_401_UNAUTHORIZED)
return await BaseQueryResolver(PointOfInterest, models.PointOfInterest).get_one(
id, info.context.user_id,
only_public=public
@@ -6,9 +6,6 @@ from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
from graphql_schema.entities.types.mutation_input import EditAircraftInput, CreateAircraftInput
from graphql_schema.entities.types.types import Aircraft
from paths import AIRCRAFT_UPLOAD_DEST_PATH
from utils.file import delete_file
from utils.upload import handle_file_upload
class AircraftQueryResolver(BaseQueryResolver):
@@ -1,16 +1,15 @@
import os
import shutil
from typing import Type, Optional, List
from typing import Optional
from PIL import Image
from pydantic import BaseModel
from sqlalchemy import select, delete, insert
from sqlalchemy import delete, insert
from background_jobs.elevation import add_terrain_elevation_to_photo
from background_jobs.photo import generate_thumbnail, resize_photo
from database import models
from database.transaction import get_session
from graphql_schema.entities.helpers.combobox import handle_combobox_save
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver, GQL_TYPE
from graphql_schema.entities.resolvers.base import BaseMutationResolver, BaseQueryResolver
from graphql_schema.entities.types.mutation_input import EditPhotoInput, UploadPhotoInput, AdjustmentInput
from graphql_schema.entities.types.types import Photo
from paths import get_photo_basepath
+3 -4
View File
@@ -10,7 +10,8 @@ from graphql_schema.dataloaders.multi_models import (
poi_photos_dataloader, flight_by_poi_dataloader, flight_copilots_dataloader, flight_track_dataloader,
photos_dataloader, flights_by_aircraft_dataloader, users_in_organization_dataloader,
aircrafts_from_organization_dataloader, user_organizations_dataloader, flights_by_event_dataloader,
flights_by_copilot_dataloader, public_flights_by_event_dataloader, public_flights_by_copilot_dataloader, photo_copilots_dataloader, photos_aircraft_dataloader, copilots_in_photo_dataloader
flights_by_copilot_dataloader, public_flights_by_event_dataloader, public_flights_by_copilot_dataloader,
photo_copilots_dataloader, photos_aircraft_dataloader, copilots_in_photo_dataloader
)
from graphql_schema.dataloaders.single_model import (
poi_dataloader, poi_type_dataloader, event_dataloader, aircraft_dataloader, airport_dataloader,
@@ -18,9 +19,7 @@ from graphql_schema.dataloaders.single_model import (
photo_dataloader, user_dataloader
)
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
from paths import (
get_public_url, get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH
)
from paths import get_avatar_url, get_title_image_url, get_photo_thumbnail_url, get_photo_url, FLIGHT_GPX_TRACK_PATH
@strawberry.type
+3 -7
View File
@@ -1,7 +1,7 @@
import sentry_sdk
from datetime import timedelta
from typing import Optional
from fastapi import FastAPI, APIRouter, Depends, Security
from fastapi import FastAPI, APIRouter, Depends, Security, HTTPException
from fastapi_jwt import JwtAuthorizationCredentials, JwtAccessBearerCookie, JwtRefreshBearerCookie
from graphql import GraphQLError
from sqlalchemy import select
@@ -10,10 +10,7 @@ from starlette.middleware.cors import CORSMiddleware
from starlette.responses import RedirectResponse, Response
from starlette.staticfiles import StaticFiles
from strawberry.fastapi import GraphQLRouter
from config import (
APP_SECRET_KEY, GRAPHIQL, APP_DEBUG, ALLOW_CORS_ORIGINS, SENTRY_DSN, ACCESS_TOKEN_VALIDITY_MINUTES,
REFRESH_TOKEN_VALIDITY_DAYS
)
from config import APP_SECRET_KEY, GRAPHIQL, APP_DEBUG, ALLOW_CORS_ORIGINS, SENTRY_DSN, REFRESH_TOKEN_VALIDITY_DAYS
from database import models, async_session
from endpoints.login import LoginEndpoint, LoginInput, RefreshEndpoint, LogoutEndpoint
from endpoints.photo_editor_preview import PhotoEditorEndpoint
@@ -26,7 +23,6 @@ class App:
access_security = JwtAccessBearerCookie(
secret_key=APP_SECRET_KEY,
auto_error=False,
access_expires_delta=timedelta(minutes=ACCESS_TOKEN_VALIDITY_MINUTES),
)
refresh_security = JwtRefreshBearerCookie(
secret_key=APP_SECRET_KEY,
@@ -39,7 +35,7 @@ class App:
sentry_sdk.init(
dsn=SENTRY_DSN,
enable_tracing=True,
ignore_errors = [GraphQLError]
ignore_errors=[GraphQLError, HTTPException]
)
app = FastAPI()