Refactor/upgrade backend and frontend parts (#2)

* ♻️ Refactor and simplify backend code

* ♻️ Refactor frontend state, integrate typesafe-vuex accessors into state files

* ♻️ Use new state accessors and standardize layout

* 🔒 Upgrade and fix npm security audit

* 🔧 Update local re-generation scripts

* 🔊 Log startup exceptions to detect errors early

* ✏️ Fix password reset token content

* 🔥 Remove unneeded Dockerfile directives

* 🔥 Remove unnecessary print

* 🔥 Remove unnecessary code, upgrade dependencies in backend

* ✏️ Fix typos in docstrings and comments

* 🏗️ Improve user Depends utilities to simplify and remove code

* 🔥 Remove deprecated SQLAlchemy parameter
This commit is contained in:
Sebastián Ramírez
2019-03-11 13:36:42 +04:00
committed by GitHub
parent 9e0b826618
commit cd112bd683
54 changed files with 492 additions and 371 deletions

View File

@@ -1,10 +1,8 @@
from fastapi import APIRouter
from app.api.api_v1.endpoints.token import router as token_router
from app.api.api_v1.endpoints.user import router as user_router
from app.api.api_v1.endpoints.utils import router as utils_router
from app.api.api_v1.endpoints import token, user, utils
api_router = APIRouter()
api_router.include_router(token_router)
api_router.include_router(user_router)
api_router.include_router(utils_router)
api_router.include_router(token.router)
api_router.include_router(user.router)
api_router.include_router(utils.router)

View File

@@ -4,12 +4,12 @@ from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app import crud
from app.api.utils.db import get_db
from app.api.utils.security import get_current_user
from app.core import config
from app.core.jwt import create_access_token
from app.core.security import get_password_hash
from app.crud import user as crud_user
from app.db_models.user import User as DBUser
from app.models.msg import Msg
from app.models.token import Token
@@ -30,12 +30,12 @@ def login_access_token(
"""
OAuth2 compatible token login, get an access token for future requests
"""
user = crud_user.authenticate(
user = crud.user.authenticate(
db, email=form_data.username, password=form_data.password
)
if not user:
raise HTTPException(status_code=400, detail="Incorrect email or password")
elif not crud_user.is_active(user):
elif not crud.user.is_active(user):
raise HTTPException(status_code=400, detail="Inactive user")
access_token_expires = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
return {
@@ -59,7 +59,7 @@ def recover_password(email: str, db: Session = Depends(get_db)):
"""
Password Recovery
"""
user = crud_user.get_by_email(db, email=email)
user = crud.user.get_by_email(db, email=email)
if not user:
raise HTTPException(
@@ -81,13 +81,13 @@ def reset_password(token: str, new_password: str, db: Session = Depends(get_db))
email = verify_password_reset_token(token)
if not email:
raise HTTPException(status_code=400, detail="Invalid token")
user = crud_user.get_by_email(db, email=email)
user = crud.user.get_by_email(db, email=email)
if not user:
raise HTTPException(
status_code=404,
detail="The user with this username does not exist in the system.",
)
elif not crud_user.is_active(user):
elif not crud.user.is_active(user):
raise HTTPException(status_code=400, detail="Inactive user")
hashed_password = get_password_hash(new_password)
user.hashed_password = hashed_password

View File

@@ -5,10 +5,10 @@ from fastapi.encoders import jsonable_encoder
from pydantic.types import EmailStr
from sqlalchemy.orm import Session
from app import crud
from app.api.utils.db import get_db
from app.api.utils.security import get_current_user
from app.api.utils.security import get_current_active_superuser, get_current_active_user
from app.core import config
from app.crud import user as crud_user
from app.db_models.user import User as DBUser
from app.models.user import User, UserInCreate, UserInDB, UserInUpdate
from app.utils import send_new_account_email
@@ -21,18 +21,12 @@ def read_users(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: DBUser = Depends(get_current_user),
current_user: DBUser = Depends(get_current_active_superuser),
):
"""
Retrieve users
"""
if not crud_user.is_active(current_user):
raise HTTPException(status_code=400, detail="Inactive user")
elif not crud_user.is_superuser(current_user):
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
users = crud_user.get_multi(db, skip=skip, limit=limit)
users = crud.user.get_multi(db, skip=skip, limit=limit)
return users
@@ -41,24 +35,18 @@ def create_user(
*,
db: Session = Depends(get_db),
user_in: UserInCreate,
current_user: DBUser = Depends(get_current_user),
current_user: DBUser = Depends(get_current_active_superuser),
):
"""
Create new user
"""
if not crud_user.is_active(current_user):
raise HTTPException(status_code=400, detail="Inactive user")
elif not crud_user.is_superuser(current_user):
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
user = crud_user.get_by_email(db, email=user_in.email)
user = crud.user.get_by_email(db, email=user_in.email)
if user:
raise HTTPException(
status_code=400,
detail="The user with this username already exists in the system.",
)
user = crud_user.create(db, user_in=user_in)
user = crud.user.create(db, user_in=user_in)
if config.EMAILS_ENABLED and user_in.email:
send_new_account_email(
email_to=user_in.email, username=user_in.email, password=user_in.password
@@ -73,13 +61,11 @@ def update_user_me(
password: str = Body(None),
full_name: str = Body(None),
email: EmailStr = Body(None),
current_user: DBUser = Depends(get_current_user),
current_user: DBUser = Depends(get_current_active_user),
):
"""
Update own user
"""
if not crud_user.is_active(current_user):
raise HTTPException(status_code=400, detail="Inactive user")
current_user_data = jsonable_encoder(current_user)
user_in = UserInUpdate(**current_user_data)
if password is not None:
@@ -88,19 +74,18 @@ def update_user_me(
user_in.full_name = full_name
if email is not None:
user_in.email = email
user = crud_user.update(db, user=current_user, user_in=user_in)
user = crud.user.update(db, user=current_user, user_in=user_in)
return user
@router.get("/users/me", tags=["users"], response_model=User)
def read_user_me(
db: Session = Depends(get_db), current_user: DBUser = Depends(get_current_user)
db: Session = Depends(get_db),
current_user: DBUser = Depends(get_current_active_user),
):
"""
Get current user
"""
if not crud_user.is_active(current_user):
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
@@ -120,32 +105,30 @@ def create_user_open(
status_code=403,
detail="Open user resgistration is forbidden on this server",
)
user = crud_user.get_by_email(db, email=email)
user = crud.user.get_by_email(db, email=email)
if user:
raise HTTPException(
status_code=400,
detail="The user with this username already exists in the system",
)
user_in = UserInCreate(password=password, email=email, full_name=full_name)
user = crud_user.create(db, user_in=user_in)
user = crud.user.create(db, user_in=user_in)
return user
@router.get("/users/{user_id}", tags=["users"], response_model=User)
def read_user_by_id(
user_id: int,
current_user: DBUser = Depends(get_current_user),
current_user: DBUser = Depends(get_current_active_user),
db: Session = Depends(get_db),
):
"""
Get a specific user by username (email)
"""
if not crud_user.is_active(current_user):
raise HTTPException(status_code=400, detail="Inactive user")
user = crud_user.get(db, user_id=user_id)
user = crud.user.get(db, user_id=user_id)
if user == current_user:
return user
if not crud_user.is_superuser(current_user):
if not crud.user.is_superuser(current_user):
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
@@ -158,23 +141,17 @@ def update_user(
db: Session = Depends(get_db),
user_id: int,
user_in: UserInUpdate,
current_user: UserInDB = Depends(get_current_user),
current_user: UserInDB = Depends(get_current_active_superuser),
):
"""
Update a user
"""
if not crud_user.is_active(current_user):
raise HTTPException(status_code=400, detail="Inactive user")
elif not crud_user.is_superuser(current_user):
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
user = crud_user.get(db, user_id=user_id)
user = crud.user.get(db, user_id=user_id)
if not user:
raise HTTPException(
status_code=404,
detail="The user with this username does not exist in the system",
)
user = crud_user.update(db, user=user, user_in=user_in)
user = crud.user.update(db, user=user, user_in=user_in)
return user

View File

@@ -1,9 +1,8 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends
from pydantic.types import EmailStr
from app.api.utils.security import get_current_user
from app.api.utils.security import get_current_active_superuser
from app.core.celery_app import celery_app
from app.crud import user as crud_user
from app.models.msg import Msg
from app.models.user import UserInDB
from app.utils import send_test_email
@@ -12,22 +11,22 @@ router = APIRouter()
@router.post("/test-celery/", tags=["utils"], response_model=Msg, status_code=201)
def test_celery(msg: Msg, current_user: UserInDB = Depends(get_current_user)):
def test_celery(
msg: Msg, current_user: UserInDB = Depends(get_current_active_superuser)
):
"""
Test Celery worker
"""
if not crud_user.is_superuser(current_user):
raise HTTPException(status_code=400, detail="Not a superuser")
celery_app.send_task("app.worker.test_celery", args=[msg.msg])
return {"msg": "Word received"}
@router.post("/test-email/", tags=["utils"], response_model=Msg, status_code=201)
def test_email(email_to: EmailStr, current_user: UserInDB = Depends(get_current_user)):
def test_email(
email_to: EmailStr, current_user: UserInDB = Depends(get_current_active_superuser)
):
"""
Test emails
"""
if not crud_user.is_superuser(current_user):
raise HTTPException(status_code=400, detail="Not a superuser")
send_test_email(email_to=email_to)
return {"msg": "Test email sent"}

View File

@@ -5,10 +5,11 @@ from jwt import PyJWTError
from sqlalchemy.orm import Session
from starlette.status import HTTP_403_FORBIDDEN
from app import crud
from app.api.utils.db import get_db
from app.core import config
from app.core.jwt import ALGORITHM
from app.crud import user as crud_user
from app.db_models.user import User
from app.models.token import TokenPayload
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/login/access-token")
@@ -24,7 +25,21 @@ def get_current_user(
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
)
user = crud_user.get(db, user_id=token_data.user_id)
user = crud.user.get(db, user_id=token_data.user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
def get_current_active_user(current_user: User = Security(get_current_user)):
if not crud.user.is_active(current_user):
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
def get_current_active_superuser(current_user: User = Security(get_current_user)):
if not crud.user.is_superuser(current_user):
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
return current_user

View File

@@ -18,8 +18,12 @@ wait_seconds = 1
after=after_log(logger, logging.WARN),
)
def init():
# Try to create session to check if DB is awake
db_session.execute("SELECT 1")
try:
# Try to create session to check if DB is awake
db_session.execute("SELECT 1")
except Exception as e:
logger.error(e)
raise e
def main():

View File

@@ -18,8 +18,12 @@ wait_seconds = 1
after=after_log(logger, logging.WARN),
)
def init():
# Try to create session to check if DB is awake
db_session.execute("SELECT 1")
try:
# Try to create session to check if DB is awake
db_session.execute("SELECT 1")
except Exception as e:
logger.error(e)
raise e
def main():

View File

@@ -0,0 +1 @@
from . import user

View File

@@ -1,4 +1,4 @@
from typing import List, Union
from typing import List, Optional
from fastapi.encoders import jsonable_encoder
@@ -7,20 +7,20 @@ from app.db_models.user import User
from app.models.user import UserInCreate, UserInUpdate
def get(db_session, *, user_id: int) -> Union[User, None]:
def get(db_session, *, user_id: int) -> Optional[User]:
return db_session.query(User).filter(User.id == user_id).first()
def get_by_email(db_session, *, email: str) -> Union[User, None]:
def get_by_email(db_session, *, email: str) -> Optional[User]:
return db_session.query(User).filter(User.email == email).first()
def authenticate(db_session, *, email: str, password: str) -> Union[User, bool]:
def authenticate(db_session, *, email: str, password: str) -> Optional[User]:
user = get_by_email(db_session, email=email)
if not user:
return False
return None
if not verify_password(password, user.hashed_password):
return False
return None
return user
@@ -32,7 +32,7 @@ def is_superuser(user) -> bool:
return user.is_superuser
def get_multi(db_session, *, skip=0, limit=100) -> Union[List[User], List[None]]:
def get_multi(db_session, *, skip=0, limit=100) -> List[Optional[User]]:
return db_session.query(User).offset(skip).limit(limit).all()

View File

@@ -1,19 +1,19 @@
from app import crud
from app.core import config
from app.crud import user as crud_user
from app.models.user import UserInCreate
def init_db(db_session):
# Tables should be created with Alembic migrations
# But if you don't want to use migrations, create
# the tables uncommenting the next line
# the tables un-commenting the next line
# Base.metadata.create_all(bind=engine)
user = crud_user.get_by_email(db_session, email=config.FIRST_SUPERUSER)
user = crud.user.get_by_email(db_session, email=config.FIRST_SUPERUSER)
if not user:
user_in = UserInCreate(
email=config.FIRST_SUPERUSER,
password=config.FIRST_SUPERUSER_PASSWORD,
is_superuser=True,
)
user = crud_user.create(db_session, user_in=user_in)
user = crud.user.create(db_session, user_in=user_in)

View File

@@ -3,7 +3,7 @@ from sqlalchemy.orm import scoped_session, sessionmaker
from app.core import config
engine = create_engine(config.SQLALCHEMY_DATABASE_URI, convert_unicode=True)
engine = create_engine(config.SQLALCHEMY_DATABASE_URI)
db_session = scoped_session(
sessionmaker(autocommit=False, autoflush=False, bind=engine)
)

View File

@@ -1,7 +1,7 @@
import requests
from app import crud
from app.core import config
from app.crud import user as crud_user
from app.db.session import db_session
from app.models.user import UserInCreate
from app.tests.utils.user import user_authentication_headers
@@ -32,7 +32,7 @@ def test_create_user_new_email(superuser_token_headers):
)
assert 200 <= r.status_code < 300
created_user = r.json()
user = crud_user.get_by_email(db_session, email=username)
user = crud.user.get_by_email(db_session, email=username)
assert user.email == created_user["email"]
@@ -41,7 +41,7 @@ def test_get_existing_user(superuser_token_headers):
username = random_lower_string()
password = random_lower_string()
user_in = UserInCreate(email=username, password=password)
user = crud_user.create(db_session, user_in=user_in)
user = crud.user.create(db_session, user_in=user_in)
user_id = user.id
r = requests.get(
f"{server_api}{config.API_V1_STR}/users/{user_id}",
@@ -49,7 +49,7 @@ def test_get_existing_user(superuser_token_headers):
)
assert 200 <= r.status_code < 300
api_user = r.json()
user = crud_user.get_by_email(db_session, email=username)
user = crud.user.get_by_email(db_session, email=username)
assert user.email == api_user["email"]
@@ -59,7 +59,7 @@ def test_create_user_existing_username(superuser_token_headers):
# username = email
password = random_lower_string()
user_in = UserInCreate(email=username, password=password)
user = crud_user.create(db_session, user_in=user_in)
user = crud.user.create(db_session, user_in=user_in)
data = {"email": username, "password": password}
r = requests.post(
f"{server_api}{config.API_V1_STR}/users/",
@@ -76,7 +76,7 @@ def test_create_user_by_normal_user():
username = random_lower_string()
password = random_lower_string()
user_in = UserInCreate(email=username, password=password)
user = crud_user.create(db_session, user_in=user_in)
user = crud.user.create(db_session, user_in=user_in)
user_token_headers = user_authentication_headers(server_api, username, password)
data = {"email": username, "password": password}
r = requests.post(
@@ -90,12 +90,12 @@ def test_retrieve_users(superuser_token_headers):
username = random_lower_string()
password = random_lower_string()
user_in = UserInCreate(email=username, password=password)
user = crud_user.create(db_session, user_in=user_in)
user = crud.user.create(db_session, user_in=user_in)
username2 = random_lower_string()
password2 = random_lower_string()
user_in2 = UserInCreate(email=username2, password=password2)
user2 = crud_user.create(db_session, user_in=user_in2)
user2 = crud.user.create(db_session, user_in=user_in2)
r = requests.get(
f"{server_api}{config.API_V1_STR}/users/", headers=superuser_token_headers

View File

@@ -1,6 +1,6 @@
from fastapi.encoders import jsonable_encoder
from app.crud import user as crud_user
from app import crud
from app.db.session import db_session
from app.models.user import UserInCreate
from app.tests.utils.utils import random_lower_string
@@ -10,7 +10,7 @@ def test_create_user():
email = random_lower_string()
password = random_lower_string()
user_in = UserInCreate(email=email, password=password)
user = crud_user.create(db_session, user_in=user_in)
user = crud.user.create(db_session, user_in=user_in)
assert user.email == email
assert hasattr(user, "hashed_password")
@@ -19,8 +19,8 @@ def test_authenticate_user():
email = random_lower_string()
password = random_lower_string()
user_in = UserInCreate(email=email, password=password)
user = crud_user.create(db_session, user_in=user_in)
authenticated_user = crud_user.authenticate(
user = crud.user.create(db_session, user_in=user_in)
authenticated_user = crud.user.authenticate(
db_session, email=email, password=password
)
assert authenticated_user
@@ -30,16 +30,16 @@ def test_authenticate_user():
def test_not_authenticate_user():
email = random_lower_string()
password = random_lower_string()
user = crud_user.authenticate(db_session, email=email, password=password)
assert user is False
user = crud.user.authenticate(db_session, email=email, password=password)
assert user is None
def test_check_if_user_is_active():
email = random_lower_string()
password = random_lower_string()
user_in = UserInCreate(email=email, password=password)
user = crud_user.create(db_session, user_in=user_in)
is_active = crud_user.is_active(user)
user = crud.user.create(db_session, user_in=user_in)
is_active = crud.user.is_active(user)
assert is_active is True
@@ -48,9 +48,9 @@ def test_check_if_user_is_active_inactive():
password = random_lower_string()
user_in = UserInCreate(email=email, password=password, disabled=True)
print(user_in)
user = crud_user.create(db_session, user_in=user_in)
user = crud.user.create(db_session, user_in=user_in)
print(user)
is_active = crud_user.is_active(user)
is_active = crud.user.is_active(user)
print(is_active)
assert is_active
@@ -59,8 +59,8 @@ def test_check_if_user_is_superuser():
email = random_lower_string()
password = random_lower_string()
user_in = UserInCreate(email=email, password=password, is_superuser=True)
user = crud_user.create(db_session, user_in=user_in)
is_superuser = crud_user.is_superuser(user)
user = crud.user.create(db_session, user_in=user_in)
is_superuser = crud.user.is_superuser(user)
assert is_superuser is True
@@ -68,8 +68,8 @@ def test_check_if_user_is_superuser_normal_user():
username = random_lower_string()
password = random_lower_string()
user_in = UserInCreate(email=username, password=password)
user = crud_user.create(db_session, user_in=user_in)
is_superuser = crud_user.is_superuser(user)
user = crud.user.create(db_session, user_in=user_in)
is_superuser = crud.user.is_superuser(user)
assert is_superuser is False
@@ -77,7 +77,7 @@ def test_get_user():
password = random_lower_string()
username = random_lower_string()
user_in = UserInCreate(email=username, password=password, is_superuser=True)
user = crud_user.create(db_session, user_in=user_in)
user_2 = crud_user.get(db_session, user_id=user.id)
user = crud.user.create(db_session, user_in=user_in)
user_2 = crud.user.get(db_session, user_id=user.id)
assert user.email == user_2.email
assert jsonable_encoder(user) == jsonable_encoder(user_2)

View File

@@ -19,10 +19,14 @@ wait_seconds = 1
after=after_log(logger, logging.WARN),
)
def init():
# Try to create session to check if DB is awake
db_session.execute("SELECT 1")
# Wait for API to be awake, run one simple tests to authenticate
test_get_access_token()
try:
# Try to create session to check if DB is awake
db_session.execute("SELECT 1")
# Wait for API to be awake, run one simple tests to authenticate
test_get_access_token()
except Exception as e:
logger.error(e)
raise e
def main():

View File

@@ -71,7 +71,7 @@ def send_reset_password_email(email_to: str, email: str, token: str):
def send_new_account_email(email_to: str, username: str, password: str):
project_name = config.PROJECT_NAME
subject = f"{project_name} - New acccount for user {username}"
subject = f"{project_name} - New account for user {username}"
with open(Path(config.EMAIL_TEMPLATES_DIR) / "new_account.html") as f:
template_str = f.read()
link = config.SERVER_HOST

View File

@@ -8,5 +8,4 @@ client_sentry = Client(config.SENTRY_DSN)
@celery_app.task(acks_late=True)
def test_celery(word: str):
print("test task")
return f"test task return {word}"

View File

@@ -1,2 +0,0 @@
#! /usr/bin/env bash
uvicorn app.main:app --host 0.0.0.0 --port 80 --debug

View File

@@ -1,6 +1,6 @@
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.6
RUN pip install celery==4.2.1 passlib[bcrypt] tenacity requests pydantic emails "fastapi>=0.6.0" uvicorn gunicorn pyjwt python-multipart email_validator jinja2 psycopg2-binary alembic SQLAlchemy
RUN pip install celery==4.2.1 passlib[bcrypt] tenacity requests emails "fastapi>=0.7.1" uvicorn gunicorn pyjwt python-multipart email_validator jinja2 psycopg2-binary alembic SQLAlchemy
# For development, Jupyter remote kernel, Hydrogen
# Using inside the container:

View File

@@ -1,6 +1,6 @@
FROM python:3.6
RUN pip install raven celery==4.2.1 passlib[bcrypt] tenacity requests "fastapi>=0.6.0" emails pyjwt email_validator jinja2 psycopg2-binary alembic SQLAlchemy
RUN pip install raven celery==4.2.1 passlib[bcrypt] tenacity requests "fastapi>=0.7.1" emails pyjwt email_validator jinja2 psycopg2-binary alembic SQLAlchemy
# For development, Jupyter remote kernel, Hydrogen
# Using inside the container:

View File

@@ -1,6 +1,6 @@
FROM python:3.6
RUN pip install requests pytest tenacity passlib[bcrypt] pydantic "fastapi>=0.6.0" psycopg2-binary SQLAlchemy
RUN pip install requests pytest tenacity passlib[bcrypt] "fastapi>=0.7.1" psycopg2-binary SQLAlchemy
# For development, Jupyter remote kernel, Hydrogen
# Using inside the container: