backend changes

This commit is contained in:
Martin Bauer 2021-08-29 17:57:25 +02:00
parent e5a5e141a9
commit 0d34959e08
9 changed files with 162 additions and 128 deletions

31
backend/requirements.txt Normal file
View File

@ -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

View File

@ -1,19 +1,20 @@
from .db import database, engine, Base, DbSession
from .users import add_user_routers, User
from fastapi import FastAPI from fastapi import FastAPI
from users import add_user_routers, User from .routes import create_api_router
from db import database, engine, Base, DbSession
from starlette.requests import Request 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 = FastAPI(title="swimtracker", debug=True, version="0.1")
application.include_router(api_router) fastapi_users = add_user_routers(application, database)
add_user_routers(application) 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 return application
app = get_app() app = get_app(database)
@app.middleware("http") @app.middleware("http")

View File

@ -1,7 +1,6 @@
import databases import databases
import sqlalchemy 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.ext.declarative import DeclarativeMeta, declarative_base
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from starlette.requests import Request from starlette.requests import Request

View File

@ -1,6 +1,5 @@
from db import Base from .db import Base
from sqlalchemy import Column, Integer, Index, LargeBinary, ForeignKey, and_, or_ from sqlalchemy import Column, Integer, Index, LargeBinary, ForeignKey, and_, or_
from typing import Tuple
class Session(Base): class Session(Base):

View File

@ -1,92 +1,93 @@
import base64 from . import models
from . import schemas
from .db import get_db
from fastapi import APIRouter, Depends, HTTPException 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 fastapi import status
from sqlalchemy.sql import select from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session as DbSession
router = APIRouter() from sqlalchemy.orm.exc import NoResultFound
from typing import List
from .users import User, UserTable
import base64
@router.post("/sessions", def create_api_router(current_user):
response_model=schemas.Session, router = APIRouter()
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.get("/sessions", response_model=List[schemas.Session], tags=["sessions"]) @router.post("/sessions",
def list_sessions(skip=0, response_model=schemas.Session,
limit=100, tags=["sessions"],
db: DbSession = Depends(get_db), status_code=status.HTTP_201_CREATED)
user: User = Depends(current_user)): def create_session(session: schemas.SessionBase,
return db.query(models.Session).filter(models.Session.user == user.id).order_by( db: DbSession = Depends(get_db),
models.Session.start_time.desc()).offset(skip).limit(limit).all() 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"]) @router.get("/sessions", response_model=List[schemas.Session], tags=["sessions"])
def create_friend_request(other_user_id: str, def list_sessions(skip=0,
db: DbSession = Depends(get_db), limit=100,
user: User = Depends(current_user)): db: DbSession = Depends(get_db),
if models.Friendship.are_friends(db, other_user_id, user.id): user: User = Depends(current_user)):
raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE, detail="already friends") 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, @router.post("/friends/request_friendship/{user_id}", tags=["friends"])
FR.receiving_user == user.id).count() def create_friend_request(other_user_id: str,
if friend_request_from_other_user > 0: db: DbSession = Depends(get_db),
raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE, user: User = Depends(current_user)):
detail="Friend request exist from other user, accept it") if models.Friendship.are_friends(db, other_user_id, user.id):
else: raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE, detail="already friends")
try:
new_friend_request = FR(requesting_user=user.id, receiving_user=other_user_id) FR = models.FriendRequest
db.add(new_friend_request) friend_request_from_other_user = db.query(FR).filter(FR.requesting_user == other_user_id,
db.commit() FR.receiving_user == user.id).count()
return {"msg": "ok"} if friend_request_from_other_user > 0:
except IntegrityError:
raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE, 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"]) @router.post("/friends/accept_friendship/{user_id}", tags=["friends"])
def accept_friend_request(other_user_id: str, def accept_friend_request(other_user_id: str,
db: DbSession = Depends(get_db), db: DbSession = Depends(get_db),
user: User = Depends(current_user)): user: User = Depends(current_user)):
FR = models.FriendRequest FR = models.FriendRequest
try: try:
friend_request = db.query(FR).filter(FR.requesting_user == other_user_id, friend_request = db.query(FR).filter(FR.requesting_user == other_user_id,
FR.receiving_user == user.id).one() FR.receiving_user == user.id).one()
except NoResultFound: except NoResultFound:
raise HTTPException(status_code=404, detail="No matching friend request found") raise HTTPException(status_code=404, detail="No matching friend request found")
models.Friendship.befriend(db, other_user_id, user.id) models.Friendship.befriend(db, other_user_id, user.id)
db.delete(friend_request) db.delete(friend_request)
db.commit() db.commit()
return {"msg": "ok"} return {"msg": "ok"}
@router.get("/friends", tags=["friends"], response_model=schemas.FriendsInfo) @router.get("/friends", tags=["friends"], response_model=schemas.FriendsInfo)
def list_friends_info(db: DbSession = Depends(get_db), user: User = Depends(current_user)): 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() user_obj = db.query(UserTable).filter(UserTable.id == user.id).one()
return schemas.FriendsInfo(incoming_requests=user_obj.friend_requests_in, return schemas.FriendsInfo(incoming_requests=user_obj.friend_requests_in,
outgoing_requests=user_obj.friend_requests_out) outgoing_requests=user_obj.friend_requests_out)
return router
# todo: remove friend requests # todo: remove friend requests
# todo: remove friendship # todo: remove friendship
# todo: search user by email # todo: search user by email

View File

@ -20,9 +20,6 @@ class UserInfo(BaseModel):
id: UUID4 id: UUID4
email: EmailStr email: EmailStr
class Config:
orm_mode = True
class Session(SessionBase): class Session(SessionBase):
user: UUID4 user: UUID4

View File

@ -1,12 +1,10 @@
from fastapi_users import FastAPIUsers, models from .config import JWT_SECRET
from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase from .db import Base
from fastapi_users.authentication import JWTAuthentication
from config import JWT_SECRET
from fastapi import Request 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.orm import relationship, backref
from sqlalchemy import Integer, Column
from fastapi_users.models import BaseUser
class User(models.BaseUser): class User(models.BaseUser):
@ -40,36 +38,30 @@ class UserTable(Base, SQLAlchemyBaseUserTable):
secondaryjoin=("UserTable.id == Friendship.friend_id")) secondaryjoin=("UserTable.id == Friendship.friend_id"))
user_db = SQLAlchemyUserDatabase(UserDB, database, UserTable.__table__) def add_user_routers(app, database):
jwt_authentication = JWTAuthentication(secret=JWT_SECRET, user_db = SQLAlchemyUserDatabase(UserDB, database, UserTable.__table__)
lifetime_seconds=60 * 60 * 8, jwt_authentication = JWTAuthentication(secret=JWT_SECRET,
tokenUrl="auth/jwt/login") lifetime_seconds=60 * 60 * 8,
tokenUrl="auth/jwt/login")
fastapi_users = FastAPIUsers( fastapi_users = FastAPIUsers(
user_db, user_db,
[jwt_authentication], [jwt_authentication],
User, User,
UserCreate, UserCreate,
UserUpdate, UserUpdate,
UserDB, UserDB,
) )
current_user = fastapi_users.current_user(active=True, verified=True)
current_superuser = fastapi_users.current_user(active=True, superuser=True, verified=True)
def on_after_register(user: UserDB, request: Request):
print(f"User {user.id} has registered.")
def on_after_register(user: UserDB, request: Request): def on_after_forgot_password(user: UserDB, token: str, request: Request):
print(f"User {user.id} has registered.") 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), app.include_router(fastapi_users.get_auth_router(jwt_authentication),
prefix="/auth/jwt", prefix="/auth/jwt",
tags=["auth"]) tags=["auth"])
@ -90,3 +82,4 @@ def add_user_routers(app):
tags=["auth"], tags=["auth"],
) )
app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"]) app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"])
return fastapi_users

View File

@ -2,8 +2,9 @@ import os
from typing import Any, Generator from typing import Any, Generator
import pytest import pytest
import databases
from src.db import Base, get_db 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 import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlalchemy import create_engine from sqlalchemy import create_engine
@ -14,9 +15,7 @@ from sqlalchemy.orm import sessionmaker
# database engines # database engines
SQLALCHEMY_DATABASE_URL = os.getenv('TEST_DATABASE_URL', "sqlite://") SQLALCHEMY_DATABASE_URL = os.getenv('TEST_DATABASE_URL', "sqlite://")
engine = create_engine( engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine) 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. Create a fresh database on each test case.
""" """
print(list(Base.metadata.tables.keys()))
Base.metadata.create_all(engine) # Create the tables. Base.metadata.create_all(engine) # Create the tables.
_app = get_app(databases.Database(SQLALCHEMY_DATABASE_URL))
yield _app yield _app
Base.metadata.drop_all(engine) 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 Create a new FastAPI TestClient that uses the `db_session` fixture to override
the `get_db` dependency that is injected into routes. the `get_db` dependency that is injected into routes.
""" """
def _get_test_db(): def _get_test_db():
try: try:
yield db_session yield db_session

View File

@ -1,11 +1,24 @@
from backend.src.db import DbSession from src.db import DbSession
from src.schemas import Session from src.schemas import Session
from fastapi import FastAPI from fastapi import FastAPI
from src.db import DbSession from src.db import DbSession
from fastapi.testclient import TestClient 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): # Tests to write
pass # - 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