From 0d34959e080a92a3340d399514a422b5a72c1a8f Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Sun, 29 Aug 2021 17:57:25 +0200 Subject: [PATCH] backend changes --- backend/requirements.txt | 31 ++++++ backend/src/{main.py => __init__.py} | 17 ++-- backend/src/db.py | 3 +- backend/src/models.py | 3 +- backend/src/routes.py | 147 ++++++++++++++------------- backend/src/schemas.py | 3 - backend/src/users.py | 57 +++++------ backend/tests/conftest.py | 10 +- backend/tests/test_session_api.py | 19 +++- 9 files changed, 162 insertions(+), 128 deletions(-) create mode 100644 backend/requirements.txt rename backend/src/{main.py => __init__.py} (58%) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..a170c8c --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,31 @@ +aiosqlite==0.17.0 +alembic==1.6.5 +asgiref==3.4.1 +bcrypt==3.2.0 +cffi==1.14.6 +click==8.0.1 +databases==0.4.3 +Deprecated==1.2.12 +dnspython==2.1.0 +email-validator==1.1.3 +fastapi==0.68.0 +fastapi-users==6.1.2 +h11==0.12.0 +idna==3.2 +makefun==1.11.3 +Mako==1.1.4 +MarkupSafe==2.0.1 +ormar==0.10.16 +passlib==1.7.4 +pycparser==2.20 +pydantic==1.8.2 +PyJWT==2.1.0 +python-dateutil==2.8.2 +python-editor==1.0.4 +python-multipart==0.0.5 +six==1.16.0 +SQLAlchemy==1.3.23 +starlette==0.14.2 +typing-extensions==3.7.4.3 +uvicorn==0.15.0 +wrapt==1.12.1 diff --git a/backend/src/main.py b/backend/src/__init__.py similarity index 58% rename from backend/src/main.py rename to backend/src/__init__.py index 0626ff4..7941f60 100644 --- a/backend/src/main.py +++ b/backend/src/__init__.py @@ -1,19 +1,20 @@ +from .db import database, engine, Base, DbSession +from .users import add_user_routers, User from fastapi import FastAPI -from users import add_user_routers, User -from db import database, engine, Base, DbSession +from .routes import create_api_router from starlette.requests import Request -from routes import router as api_router -import models -def get_app() -> FastAPI: +def get_app(database) -> FastAPI: application = FastAPI(title="swimtracker", debug=True, version="0.1") - application.include_router(api_router) - add_user_routers(application) + fastapi_users = add_user_routers(application, database) + current_user = fastapi_users.current_user(active=True, verified=True) + current_superuser = fastapi_users.current_user(active=True, superuser=True, verified=True) + application.include_router(create_api_router(current_user)) return application -app = get_app() +app = get_app(database) @app.middleware("http") diff --git a/backend/src/db.py b/backend/src/db.py index d7d3be2..46c8cb9 100644 --- a/backend/src/db.py +++ b/backend/src/db.py @@ -1,7 +1,6 @@ import databases import sqlalchemy -from starlette import requests -from config import DATABASE_URL +from .config import DATABASE_URL from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base from sqlalchemy.orm import sessionmaker from starlette.requests import Request diff --git a/backend/src/models.py b/backend/src/models.py index 0214c78..07d1c6f 100644 --- a/backend/src/models.py +++ b/backend/src/models.py @@ -1,6 +1,5 @@ -from db import Base +from .db import Base from sqlalchemy import Column, Integer, Index, LargeBinary, ForeignKey, and_, or_ -from typing import Tuple class Session(Base): diff --git a/backend/src/routes.py b/backend/src/routes.py index 23beab0..8894612 100644 --- a/backend/src/routes.py +++ b/backend/src/routes.py @@ -1,92 +1,93 @@ -import base64 +from . import models +from . import schemas +from .db import get_db from fastapi import APIRouter, Depends, HTTPException -from typing import List -import schemas -from users import User, UserDB, UserTable, current_user -from db import get_db -import models -from sqlalchemy.orm import Session as DbSession, lazyload -from sqlalchemy.orm.exc import NoResultFound -from sqlalchemy.exc import IntegrityError from fastapi import status -from sqlalchemy.sql import select - -router = APIRouter() +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session as DbSession +from sqlalchemy.orm.exc import NoResultFound +from typing import List +from .users import User, UserTable +import base64 -@router.post("/sessions", - response_model=schemas.Session, - tags=["sessions"], - status_code=status.HTTP_201_CREATED) -def create_session(session: schemas.SessionBase, - db: DbSession = Depends(get_db), - user: User = Depends(current_user)): - session_props = session.dict() - session_props['user'] = user.id - session_props['data'] = base64.b64decode(session_props['data']) - db_obj = models.Session(**session_props) - db.add(db_obj) - db.commit() - return db_obj +def create_api_router(current_user): + router = APIRouter() -@router.get("/sessions", response_model=List[schemas.Session], tags=["sessions"]) -def list_sessions(skip=0, - limit=100, - db: DbSession = Depends(get_db), - user: User = Depends(current_user)): - return db.query(models.Session).filter(models.Session.user == user.id).order_by( - models.Session.start_time.desc()).offset(skip).limit(limit).all() + @router.post("/sessions", + response_model=schemas.Session, + tags=["sessions"], + status_code=status.HTTP_201_CREATED) + def create_session(session: schemas.SessionBase, + db: DbSession = Depends(get_db), + user: User = Depends(current_user)): + session_props = session.dict() + session_props['user'] = user.id + session_props['data'] = base64.b64decode(session_props['data']) + db_obj = models.Session(**session_props) + db.add(db_obj) + db.commit() + return db_obj -@router.post("/friends/request_friendship/{user_id}", tags=["friends"]) -def create_friend_request(other_user_id: str, - db: DbSession = Depends(get_db), - user: User = Depends(current_user)): - if models.Friendship.are_friends(db, other_user_id, user.id): - raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE, detail="already friends") + @router.get("/sessions", response_model=List[schemas.Session], tags=["sessions"]) + def list_sessions(skip=0, + limit=100, + db: DbSession = Depends(get_db), + user: User = Depends(current_user)): + return db.query(models.Session).filter(models.Session.user == user.id).order_by( + models.Session.start_time.desc()).offset(skip).limit(limit).all() - FR = models.FriendRequest - friend_request_from_other_user = db.query(FR).filter(FR.requesting_user == other_user_id, - FR.receiving_user == user.id).count() - if friend_request_from_other_user > 0: - raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE, - detail="Friend request exist from other user, accept it") - else: - try: - new_friend_request = FR(requesting_user=user.id, receiving_user=other_user_id) - db.add(new_friend_request) - db.commit() - return {"msg": "ok"} - except IntegrityError: + + @router.post("/friends/request_friendship/{user_id}", tags=["friends"]) + def create_friend_request(other_user_id: str, + db: DbSession = Depends(get_db), + user: User = Depends(current_user)): + if models.Friendship.are_friends(db, other_user_id, user.id): + raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE, detail="already friends") + + FR = models.FriendRequest + friend_request_from_other_user = db.query(FR).filter(FR.requesting_user == other_user_id, + FR.receiving_user == user.id).count() + if friend_request_from_other_user > 0: raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE, - detail="Friend request already exists") + detail="Friend request exist from other user, accept it") + else: + try: + new_friend_request = FR(requesting_user=user.id, receiving_user=other_user_id) + db.add(new_friend_request) + db.commit() + return {"msg": "ok"} + except IntegrityError: + raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE, + detail="Friend request already exists") -@router.post("/friends/accept_friendship/{user_id}", tags=["friends"]) -def accept_friend_request(other_user_id: str, - db: DbSession = Depends(get_db), - user: User = Depends(current_user)): - FR = models.FriendRequest - try: - friend_request = db.query(FR).filter(FR.requesting_user == other_user_id, - FR.receiving_user == user.id).one() - except NoResultFound: - raise HTTPException(status_code=404, detail="No matching friend request found") + @router.post("/friends/accept_friendship/{user_id}", tags=["friends"]) + def accept_friend_request(other_user_id: str, + db: DbSession = Depends(get_db), + user: User = Depends(current_user)): + FR = models.FriendRequest + try: + friend_request = db.query(FR).filter(FR.requesting_user == other_user_id, + FR.receiving_user == user.id).one() + except NoResultFound: + raise HTTPException(status_code=404, detail="No matching friend request found") - models.Friendship.befriend(db, other_user_id, user.id) - db.delete(friend_request) - db.commit() - return {"msg": "ok"} + models.Friendship.befriend(db, other_user_id, user.id) + db.delete(friend_request) + db.commit() + return {"msg": "ok"} -@router.get("/friends", tags=["friends"], response_model=schemas.FriendsInfo) -def list_friends_info(db: DbSession = Depends(get_db), user: User = Depends(current_user)): - user_obj = db.query(UserTable).filter(UserTable.id == user.id).one() - return schemas.FriendsInfo(incoming_requests=user_obj.friend_requests_in, - outgoing_requests=user_obj.friend_requests_out) - + @router.get("/friends", tags=["friends"], response_model=schemas.FriendsInfo) + def list_friends_info(db: DbSession = Depends(get_db), user: User = Depends(current_user)): + user_obj = db.query(UserTable).filter(UserTable.id == user.id).one() + return schemas.FriendsInfo(incoming_requests=user_obj.friend_requests_in, + outgoing_requests=user_obj.friend_requests_out) + return router # todo: remove friend requests # todo: remove friendship # todo: search user by email diff --git a/backend/src/schemas.py b/backend/src/schemas.py index a15ebb7..dc0c732 100644 --- a/backend/src/schemas.py +++ b/backend/src/schemas.py @@ -20,9 +20,6 @@ class UserInfo(BaseModel): id: UUID4 email: EmailStr - class Config: - orm_mode = True - class Session(SessionBase): user: UUID4 diff --git a/backend/src/users.py b/backend/src/users.py index 19d4e6a..6a9edce 100644 --- a/backend/src/users.py +++ b/backend/src/users.py @@ -1,12 +1,10 @@ -from fastapi_users import FastAPIUsers, models -from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase -from fastapi_users.authentication import JWTAuthentication -from config import JWT_SECRET +from .config import JWT_SECRET +from .db import Base from fastapi import Request -from db import database, Base +from fastapi_users import FastAPIUsers, models +from fastapi_users.authentication import JWTAuthentication +from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase from sqlalchemy.orm import relationship, backref -from sqlalchemy import Integer, Column -from fastapi_users.models import BaseUser class User(models.BaseUser): @@ -40,36 +38,30 @@ class UserTable(Base, SQLAlchemyBaseUserTable): secondaryjoin=("UserTable.id == Friendship.friend_id")) -user_db = SQLAlchemyUserDatabase(UserDB, database, UserTable.__table__) -jwt_authentication = JWTAuthentication(secret=JWT_SECRET, - lifetime_seconds=60 * 60 * 8, - tokenUrl="auth/jwt/login") +def add_user_routers(app, database): + user_db = SQLAlchemyUserDatabase(UserDB, database, UserTable.__table__) + jwt_authentication = JWTAuthentication(secret=JWT_SECRET, + lifetime_seconds=60 * 60 * 8, + tokenUrl="auth/jwt/login") -fastapi_users = FastAPIUsers( - user_db, - [jwt_authentication], - User, - UserCreate, - UserUpdate, - UserDB, -) -current_user = fastapi_users.current_user(active=True, verified=True) -current_superuser = fastapi_users.current_user(active=True, superuser=True, verified=True) + fastapi_users = FastAPIUsers( + user_db, + [jwt_authentication], + User, + UserCreate, + UserUpdate, + UserDB, + ) + def on_after_register(user: UserDB, request: Request): + print(f"User {user.id} has registered.") -def on_after_register(user: UserDB, request: Request): - print(f"User {user.id} has registered.") + def on_after_forgot_password(user: UserDB, token: str, request: Request): + print(f"User {user.id} has forgot their password. Reset token: {token}") + def after_verification_request(user: UserDB, token: str, request: Request): + print(f"Verification requested for user {user.id}. Verification token: {token}") -def on_after_forgot_password(user: UserDB, token: str, request: Request): - print(f"User {user.id} has forgot their password. Reset token: {token}") - - -def after_verification_request(user: UserDB, token: str, request: Request): - print(f"Verification requested for user {user.id}. Verification token: {token}") - - -def add_user_routers(app): app.include_router(fastapi_users.get_auth_router(jwt_authentication), prefix="/auth/jwt", tags=["auth"]) @@ -90,3 +82,4 @@ def add_user_routers(app): tags=["auth"], ) app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"]) + return fastapi_users \ No newline at end of file diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 021dc1f..cdf5992 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -2,8 +2,9 @@ import os from typing import Any, Generator import pytest +import databases from src.db import Base, get_db -from src.main import app as _app +from src import get_app from fastapi import FastAPI from fastapi.testclient import TestClient from sqlalchemy import create_engine @@ -14,9 +15,7 @@ from sqlalchemy.orm import sessionmaker # database engines SQLALCHEMY_DATABASE_URL = os.getenv('TEST_DATABASE_URL', "sqlite://") -engine = create_engine( - SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} -) +engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) Session = sessionmaker(autocommit=False, autoflush=False, bind=engine) @@ -26,7 +25,9 @@ def app() -> Generator[FastAPI, Any, None]: """ Create a fresh database on each test case. """ + print(list(Base.metadata.tables.keys())) Base.metadata.create_all(engine) # Create the tables. + _app = get_app(databases.Database(SQLALCHEMY_DATABASE_URL)) yield _app Base.metadata.drop_all(engine) @@ -61,7 +62,6 @@ def client(app: FastAPI, db_session: Session) -> Generator[TestClient, Any, None Create a new FastAPI TestClient that uses the `db_session` fixture to override the `get_db` dependency that is injected into routes. """ - def _get_test_db(): try: yield db_session diff --git a/backend/tests/test_session_api.py b/backend/tests/test_session_api.py index 27227aa..283512e 100644 --- a/backend/tests/test_session_api.py +++ b/backend/tests/test_session_api.py @@ -1,11 +1,24 @@ -from backend.src.db import DbSession +from src.db import DbSession from src.schemas import Session from fastapi import FastAPI from src.db import DbSession from fastapi.testclient import TestClient +from src.users import UserCreate, User +from fastapi.encoders import jsonable_encoder #---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -def test_session_create(app: FastAPI, db_session: DbSession, client: TestClient): - pass +# Tests to write +# - User Flow: register, verify, login +# - create, list, delete session +# - friendship: query user by mail, create friend request, accept +def test_register_user(app: FastAPI, db_session: DbSession, client: TestClient): + req_data = jsonable_encoder(UserCreate(email="test@abc.com", password="password")) + response = client.post("/auth/register", json=req_data) + print(response.json()) + resp_user = User(**response.json()) + assert response.status_code == 201 + assert resp_user.is_active + assert not resp_user.is_superuser + assert not resp_user.is_verified