Prvni pokus o verejny pristup, sprava profilu

This commit is contained in:
Michal Kváček
2023-08-09 00:27:27 +02:00
parent ca500472e3
commit 57afde90c3
15 changed files with 814 additions and 124 deletions
+5 -4
View File
@@ -45,8 +45,6 @@ class BaseModel:
return obj
# TODO: doplnit GPX k letu
user_is_in_organization = Table(
"user_is_in_organization",
BaseModel.metadata,
@@ -219,7 +217,7 @@ class Flight(BaseModel):
weather_info_takeoff_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
weather_info_landing_id: Mapped[int] = mapped_column(Integer, ForeignKey('weather_info.id'), nullable=True)
landings: Mapped[int] = mapped_column(Integer, default=1)
is_public: Mapped[bool] = mapped_column(Boolean, server_default='0')
created_by_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default='0')
@@ -282,7 +280,10 @@ class User(BaseModel):
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
avatar_image_url: Mapped[str] = mapped_column(String(128), nullable=True)
description: Mapped[str] = mapped_column(Text, nullable=False)
public_username: Mapped[str] = mapped_column(String(128), nullable=True, unique=True)
avatar_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
title_image_filename: Mapped[str] = mapped_column(String(128), nullable=True)
password_hashed: Mapped[str] = mapped_column(String(60), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
-60
View File
@@ -1,60 +0,0 @@
from database.models import User, Flight, Copilot, Airport, Aircraft
from endpoints.base import BaseEndpoint
class InitDataEndpoint(BaseEndpoint):
async def on_get(self):
user_ids = []
copilot_ids = []
flight_ids = []
users = [
User(avatar_image_url="", name="Karel Vomacka", email="a@test.cz", password_hashed="****"),
User(avatar_image_url="", name="Karel Novak", email="b@test.cz", password_hashed="****"),
User(avatar_image_url="", name="Franta Pavel", email="c@test.cz", password_hashed="****"),
]
airport = Airport(name="Letiste Letnany", icao_code="LKLT")
self.db.add(airport)
for user in users:
self.db.add(user)
await self.db.flush()
user_ids.append(user.id)
aircraft = Aircraft(name="OK-AUR28", type="Bristell NG5", description="", created_by=user)
self.db.add(aircraft)
copilot = None
if user.name == 'Franta Pavel':
copilot = Copilot(name="Copilot test", created_by_id=user.id)
self.db.add(copilot)
await self.db.flush()
copilot_ids.append(copilot.id)
flight = Flight(name="test flight", description="Testovaci popis", duration_total=65, duration_pic=65,
takeoff_airport=airport, landing_airport=airport, aircraft=aircraft, created_by_id=user.id,
copilot_id=copilot.id if copilot else None)
self.db.add(flight)
await self.db.flush()
flight_ids.append(flight.id)
return {
"user_ids": user_ids,
"copilot_ids": copilot_ids,
"flight_ids": flight_ids,
'airport_id': airport.id
}
#
#
# self.db.add(Flight(name="test"))
# await self.db.flush()
#
# data = await self.db.execute(select(Flight).order_by(desc(Flight.id)))
# model = data.scalars().first()
# return {
# "status": model
# }
-13
View File
@@ -4,7 +4,6 @@ from fastapi_jwt.jwt import JwtAccess, JwtRefresh
from passlib.hash import bcrypt
from sqlalchemy import select
from starlette.responses import Response
from database.models import User
from endpoints.base import BaseEndpoint
from pydantic import BaseModel
@@ -74,15 +73,3 @@ class RefreshEndpoint(BaseEndpoint):
"access_token": access_token,
"access_token_validity": self.access_security.access_expires_delta.total_seconds(),
}
class MeEndpoint(BaseEndpoint):
async def on_get(self, credentials) -> dict:
if not credentials:
raise HTTPException(status_code=401)
query = select(User).filter_by(id=credentials['id'])
user = (await self.db.scalars(query)).first()
return user.as_dict()
-1
View File
@@ -1,5 +1,4 @@
import re
from fastapi import HTTPException
from sqlalchemy import select
from typing import Optional
+23 -11
View File
@@ -15,7 +15,7 @@ from graphql_schema.entities.airport import Airport
from graphql_schema.entities.photo import Photo
from graphql_schema.entities.poi import PointOfInterest
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input
from upload_utils import get_public_url, handle_file_upload, check_directories, file_exists, delete_file
from upload_utils import handle_file_upload, file_exists, delete_file
from .helpers.flight import handle_aircraft_save, handle_track_edit, handle_copilot_edit, handle_weather_info
from ..dataloaders.weather import airport_weather_info_loader
from ..types import ComboboxInput
@@ -95,28 +95,40 @@ class Flight:
gpx_track_url: Optional[str] = strawberry.field(resolver=load_gpx_track_url)
def get_base_query(user_id: int):
return (
def get_base_query(user_id: Optional[int], username: Optional[str] = None, is_auth: bool = False):
query = (
select(models.Flight)
.filter(models.Flight.created_by_id == user_id)
.filter(models.Flight.deleted.is_(False))
.order_by(models.Flight.id.desc())
)
if user_id:
query = query.filter(models.Flight.created_by_id == user_id)
if username:
query = query.filter(models.Flight.created_by.public_username == username)
if not is_auth:
query = query.filter(models.Flight.is_public.is_(True))
return query
@strawberry.type
class FlightQueries:
@strawberry.field
async def flights(root, info) -> List[Flight]:
query = get_base_query(info.context.user_id).order_by(models.Flight.id.desc())
async def flights(root, info, username: Optional[str] = None) -> List[Flight]:
query = (
get_base_query(user_id=info.context.user_id, username=username, is_auth=bool(info.context.user_id))
.order_by(models.Flight.id.desc())
)
return (await info.context.db.scalars(query)).all()
@strawberry.field
async def flight(root, info, id: int) -> Flight:
async def flight(root, info, id: int, username: Optional[str] = None) -> Flight:
query = (
get_base_query(info.context.user_id)
get_base_query(user_id=info.context.user_id, username=username, is_auth=bool(info.context.user_id))
.filter(models.Flight.id == id)
.filter(models.Flight.deleted.is_(False))
)
@@ -176,7 +188,7 @@ class EditFlightMutation:
# TODO: umoznit editovat jen vlastni lety!
flight = (await info.context.db.scalars(
get_base_query(info.context.user_id).filter(models.Flight.id == id)
get_base_query(user_id=info.context.user_id, is_auth=bool(info.context.user_id)).filter(models.Flight.id == id)
)).one()
data = input.to_dict()
@@ -221,7 +233,7 @@ class DeleteFlightMutation:
async def delete_flight(self, info, id: int) -> Flight:
flight = (
(await info.context.db.scalars(
get_base_query(info.context.user_id)
get_base_query(user_id=info.context.user_id, is_auth=True)
.filter(models.Flight.id == id))
)
.one()
+76 -11
View File
@@ -1,25 +1,90 @@
from typing import Optional
import strawberry
from graphql import GraphQLError
from passlib.hash import bcrypt
from sqlalchemy import select
from strawberry.file_uploads import Upload
from database import models
from database.models import User
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type
from graphql_schema.sqlalchemy_to_strawberry_type import strawberry_sqlalchemy_type, strawberry_sqlalchemy_input
from upload_utils import handle_file_upload, delete_file
@strawberry_sqlalchemy_type(User, exclude_fields=['password_hashed'])
class UserType:
pass
class User:
async def load_avatar_image_url(root):
if not root.avatar_image_filename:
return None
@strawberry.type
class LoginResultType:
logged_user = UserType
access_token: str
refresh_token: str
return f"http://localhost:8000/uploads/profile/{root.id}/{root.avatar_image_filename}"
async def load_title_image_url(root):
if not root.title_image_filename:
return "http://localhost:8000/static/default-title-image.jpg"
return f"http://localhost:8000/uploads/profile/{root.id}/{root.title_image_filename}"
avatar_image_url: Optional[str] = strawberry.field(resolver=load_avatar_image_url)
title_image_url: str = strawberry.field(resolver=load_title_image_url)
@strawberry.type
class UserQueries:
@strawberry.field
async def user(root, info, username: str) -> User:
print("username")
if len(username) == 0:
raise GraphQLError("Username not set!")
return (await info.context.db.scalars(
select(models.User).filter_by(public_username=username)
)).one()
@strawberry.field
async def logged_user(root, info) -> UserType:
query = select(User).filter_by(id=info.context.user_id)
return (await info.context.db.scalars(query)).first()
async def logged_user(root, info) -> User:
return (await info.context.db.scalars(
select(models.User).filter_by(id=info.context.user_id)
)).one()
@strawberry.type
class EditUserMutation:
@strawberry_sqlalchemy_input(
models.User,
exclude_fields=['id', 'email', 'avatar_image_filename', 'password_hashed', 'title_image_filename'],
all_optional=True
)
class EditUserInput:
old_password: Optional[str] = None
new_password: Optional[str] = None
avatar_image: Optional[Upload] = None
title_image: Optional[Upload] = None
@strawberry.mutation
async def edit_logged_user(root, info, input: EditUserInput) -> User:
user = (await info.context.db.scalars(
select(models.User).filter_by(id=info.context.user_id)
)).one()
user_image_path = f"/app/uploads/profile/{user.id}"
data = input.to_dict()
if input.avatar_image:
if user.avatar_image_filename:
delete_file(f"{user_image_path}/{user.avatar_image_filename}", silent=True)
data['avatar_image_filename'] = await handle_file_upload(input.avatar_image, user_image_path)
if input.title_image:
if user.title_image_filename:
delete_file(f"{user_image_path}/{user.title_image_filename}", silent=True)
data['title_image_filename'] = await handle_file_upload(input.title_image, user_image_path)
if input.old_password and input.new_password:
if not bcrypt.verify(input.old_password, user.password_hashed):
raise GraphQLError("Bad password")
data['password_hashed'] = bcrypt.hash(input.new_password)
return await models.User.update(info.context.db, obj=user, data=data)
+2
View File
@@ -4,6 +4,7 @@ from graphql_schema.entities.copilot import CreateCopilotMutation, EditCopilotMu
from graphql_schema.entities.flight import CreateFlightMutation, EditFlightMutation, DeleteFlightMutation
from graphql_schema.entities.photo import UploadPhotoMutation, DeletePhotoMutation, EditPhotoMutation
from graphql_schema.entities.poi import CreatePointOfInterestMutation, EditPointOfInterestMutation
from graphql_schema.entities.user import EditUserMutation
Mutation = merge_types("Mutation", (
CreateAircraftMutation,
@@ -19,4 +20,5 @@ Mutation = merge_types("Mutation", (
EditPointOfInterestMutation,
CreateCopilotMutation,
EditCopilotMutation,
EditUserMutation,
))
+9 -24
View File
@@ -10,8 +10,7 @@ from starlette.status import HTTP_401_UNAUTHORIZED
from strawberry.fastapi import GraphQLRouter
from config import APP_SECRET_KEY, GRAPHIQL, APP_DEBUG
from dependencies.db import db_session
from endpoints.init_data import InitDataEndpoint
from endpoints.login import LoginEndpoint, LoginInput, MeEndpoint, RefreshEndpoint
from endpoints.login import LoginEndpoint, LoginInput, RefreshEndpoint
from endpoints.registration import RegistrationInput, RegistrationEndpoint
from graphql_schema.schema import schema, GraphQLContext
@@ -33,6 +32,7 @@ class App:
self.setup_exception_handlers(app)
self.setup_middleware(app)
self.setup_static_paths(app)
self.setup_routes(app)
return app
@@ -43,15 +43,19 @@ class App:
@staticmethod
def setup_middleware(app: FastAPI):
print("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:9001"],
allow_origins=["http://localhost:9001"], # TODO: pridat pres ENV URL produkce
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@staticmethod
def setup_static_paths(app: FastAPI):
app.mount("/uploads", StaticFiles(directory="/app/uploads"), name="uploads")
app.mount("/static", StaticFiles(directory="/app/static"), name="static")
def setup_graphql_endpoint(self, app: FastAPI):
if APP_DEBUG:
@self.api_router.get("/graphql/autologin")
@@ -67,12 +71,9 @@ class App:
credentials: JwtAuthorizationCredentials = Security(self.access_security),
db: AsyncSession = Depends(db_session)
):
if not credentials:
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED)
return GraphQLContext(
jwt_auth_credentials=credentials,
user_id=credentials['id'],
user_id=credentials['id'] if credentials else None,
db=db,
jwt=self.access_security,
background_tasks=Depends(BackgroundTasks)
@@ -88,13 +89,6 @@ class App:
def setup_routes(self, app: FastAPI):
# protected endpoints
@self.api_router.get("/me")
async def me(
db: AsyncSession = Depends(db_session),
credentials: JwtAuthorizationCredentials = Security(self.access_security)
):
return await MeEndpoint(db).on_get(credentials)
@app.post("/refresh")
async def refresh(
resp: Response,
@@ -104,10 +98,7 @@ class App:
self.setup_graphql_endpoint(app)
app.mount("/uploads", StaticFiles(directory="/app/uploads"), name="uploads")
# public endpoints
@self.api_router.post("/login")
async def login(resp: Response, user: LoginInput, db: AsyncSession = Depends(db_session)):
return await LoginEndpoint(db, self.access_security, self.refresh_security).on_post(user, resp)
@@ -116,11 +107,5 @@ class App:
async def registration(user: RegistrationInput, db: AsyncSession = Depends(db_session)):
return await RegistrationEndpoint(db).on_post(user)
# testing endpoint
@self.api_router.get("/init-data")
async def init_data(db: AsyncSession = Depends(db_session)):
return await InitDataEndpoint(db).on_get()
# musi byt na konci
app.include_router(self.api_router)