🎉 first commit

This commit is contained in:
swve 2022-06-26 15:23:03 +02:00
parent 8c00f9a074
commit 91f4291d9b
21 changed files with 614 additions and 3 deletions

37
.gitignore vendored
View file

@ -20,7 +20,6 @@ parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
@ -50,6 +49,7 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
@ -72,6 +72,7 @@ instance/
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
@ -82,7 +83,9 @@ profile_default/
ipython_config.py
# pyenv
.python-version
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
@ -91,7 +94,22 @@ ipython_config.py
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
@ -127,3 +145,16 @@ dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

17
Dockerfile Normal file
View file

@ -0,0 +1,17 @@
#
FROM python:3.10.5
#
WORKDIR /usr/learnhouse
#
COPY ./requirements.txt /usr/learnhouse/requirements.txt
#
RUN pip install --no-cache-dir --upgrade -r /usr/learnhouse/requirements.txt
#
COPY ./ /usr/learnhouse
#
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80" , "--reload"]

0
__init__.py Normal file
View file

20
app.py Normal file
View file

@ -0,0 +1,20 @@
from typing import Union
from fastapi import FastAPI
from src import main
from fastapi.staticfiles import StaticFiles
from src.main import global_router
import pymongo
# Init
app = FastAPI(
title="LearnHouse",
description="LearnHouse is a new open-source platform tailored for learning experiences.",
version="0.1.0",
root_path="/"
)
app.include_router(global_router)
@app.get("/")
async def root():
return {"Message": "Welcome to LearnHouse ✨"}

0
config/__init__.py Normal file
View file

0
config/config.py Normal file
View file

0
config/config.yml Normal file
View file

25
docker-compose.yml Normal file
View file

@ -0,0 +1,25 @@
version: "3.9"
services:
api:
build: .
ports:
- "1338:80"
volumes:
- .:/usr/learnhouse
mongo:
image: mongo:5.0
restart: always
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=learnhouse
- MONGO_INITDB_ROOT_PASSWORD=learnhouse
mongo-express:
image: mongo-express
restart: always
ports:
- 8081:8081
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: learnhouse
ME_CONFIG_MONGODB_ADMINPASSWORD: learnhouse
ME_CONFIG_MONGODB_URL: mongodb://learnhouse:learnhouse@mongo:27017/

7
requirements.txt Normal file
View file

@ -0,0 +1,7 @@
fastapi==0.78.0
pydantic>=1.8.0,<2.0.0
uvicorn>=0.15.0,<0.16.0
pymongo==4.1.1
python-multipart
python-jose
passlib

0
src/__init__.py Normal file
View file

18
src/main.py Normal file
View file

@ -0,0 +1,18 @@
from .routers import users
from fastapi import APIRouter
from .routers import users, auth, houses
from starlette.responses import FileResponse
global_router = APIRouter(prefix="/api")
## API Routes
global_router.include_router(users.router, prefix="/users", tags=["users"])
global_router.include_router(auth.router, prefix="/auth", tags=["auth"])
global_router.include_router(houses.router, prefix="/houses", tags=["houses"])

0
src/routers/__init__.py Normal file
View file

27
src/routers/auth.py Normal file
View file

@ -0,0 +1,27 @@
from fastapi import Depends, FastAPI, APIRouter, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from ..services.auth import *
from ..services.users import *
from datetime import datetime, timedelta
router = APIRouter()
@router.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
"""
OAuth2 compatible token login, get access token for future requests
"""
user = await authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}

49
src/routers/houses.py Normal file
View file

@ -0,0 +1,49 @@
from fastapi import APIRouter, Depends
from src.services.auth import get_current_user
from src.services.houses import House, HouseInDB, create_house, get_house, get_houses, update_house, delete_house
from src.services.users import User
router = APIRouter()
@router.post("/")
async def api_create_house(house_object: House, current_user: User = Depends(get_current_user)):
"""
Create new house
"""
return await create_house(house_object, current_user)
@router.get("/{house_id}")
async def api_get_house(house_id: str):
"""
Get single House by house_id
"""
return await get_house(house_id)
@router.get("/page/{house_id}/limit/{limit}")
async def api_get_house_by(page: int, limit: int):
"""
Get houses by page and limit
"""
return await get_houses(page, limit)
@router.put("/{house_id}")
async def api_update_house(house_object: House, house_id: str, current_user: User = Depends(get_current_user)):
"""
Update House by house_id
"""
return await update_house(house_object, house_id, current_user)
@router.delete("/{house_id}")
async def api_delete_house(house_id: str, current_user: User = Depends(get_current_user)):
"""
Delete House by ID
"""
return await delete_house(house_id, current_user)

51
src/routers/users.py Normal file
View file

@ -0,0 +1,51 @@
from fastapi import Depends, FastAPI, APIRouter
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from ..services.auth import *
from ..services.users import *
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
router = APIRouter()
@router.get("/me")
async def api_get_current_user(current_user: User = Depends(get_current_user)):
"""
Get current user
"""
return current_user.dict()
@router.get("/username/{username}")
async def api_get_user_by_username(username: str):
"""
Get single user by username
"""
return await get_user(username)
@router.post("/")
async def api_create_user(user_object: UserInDB):
"""
Create new user
"""
return await create_user(user_object)
@router.delete("/username/{username}")
async def api_delete_user(username: str):
"""
Delete user by ID
"""
return await delete_user(username)
@router.put("/username/{username}")
async def api_update_user(user_object: UserInDB):
"""
Update user by ID
"""
return await update_user(user_object)

0
src/services/__init__.py Normal file
View file

64
src/services/auth.py Normal file
View file

@ -0,0 +1,64 @@
from pydantic import BaseModel
from fastapi import Depends, FastAPI, APIRouter, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from passlib.context import CryptContext
from jose import JWTError, jwt
from datetime import datetime, timedelta
from ..services.users import *
from ..services.security import *
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
#### Classes ####################################################
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str | None = None
#### Classes ####################################################
async def authenticate_user(username: str, password: str):
user = await security_get_user(username)
if not user:
return False
if not await security_verify_password(password, user.password):
return False
return user
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = await get_user(username=token_data.username)
if user is None:
raise credentials_exception
return User(**user.dict())

27
src/services/database.py Normal file
View file

@ -0,0 +1,27 @@
import pymongo
# MongoDB
client = pymongo.MongoClient("mongodb://learnhouse:learnhouse@mongo:27017/")
learnhouseDB = client["learnhouse"]
async def create_database():
learnhouseDB = client["learnhouse"]
async def check_database():
# Check if database learnhouse exists
if "learnhouse" in client.list_database_names():
return True
else:
create_database()
async def create_config_collection():
# Create config collection if it doesn't exist
learnhouseDB = client["learnhouse"]
config = learnhouseDB["config"]
config.insert_one({"name": "LearnHouse", "date": "2022"})
return config.find_one()

144
src/services/houses.py Normal file
View file

@ -0,0 +1,144 @@
import json
from typing import List
from uuid import uuid4
from pydantic import BaseModel
from src.services.users import User
from ..services.database import create_config_collection, check_database, create_database, learnhouseDB, learnhouseDB
from ..services.security import *
from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks
from datetime import datetime
#### Classes ####################################################
class House(BaseModel):
name: str
photo: str
description: str
email: str
org: str
class HouseInDB(House):
house_id: str
owners: List[str]
#### Classes ####################################################
async def get_house(house_id: str):
await check_database()
houses = learnhouseDB["houses"]
house = houses.find_one({"house_id": house_id})
if not house:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="House does not exist")
house = House(**house)
return house
async def create_house(house_object: House, current_user: User):
await check_database()
houses = learnhouseDB["houses"]
# find if house already exists using name
isHouseAvailable = houses.find_one({"name": house_object.name})
if isHouseAvailable:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="House name already exists")
# generate house_id with uuid4
house_id = str(f"house_{uuid4()}")
house = HouseInDB(house_id=house_id, owners=[
current_user.username], **house_object.dict())
house_in_db = houses.insert_one(house.dict())
if not house_in_db:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database")
return house.dict()
async def update_house(house_object: House, house_id: str, current_user: User):
await check_database()
# verify house rights
await verify_house_ownership(house_id, current_user)
houses = learnhouseDB["houses"]
house = houses.find_one({"house_id": house_id})
## get owner value from house object database
owners = house["owners"]
if not house:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="House does not exist")
updated_house = HouseInDB(house_id=house_id, owners=owners, **house_object.dict())
houses.update_one({"house_id": house_id}, {"$set": updated_house.dict()})
return HouseInDB(**updated_house.dict())
async def delete_house(house_id: str, current_user: User):
await check_database()
# verify house rights
await verify_house_ownership(house_id, current_user)
houses = learnhouseDB["houses"]
house = houses.find_one({"house_id": house_id})
if not house:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="House does not exist")
isDeleted = houses.delete_one({"house_id": house_id})
if isDeleted:
return {"detail": "House deleted"}
else:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database")
async def get_houses(page: int = 1, limit: int = 10):
await check_database()
houses = learnhouseDB["houses"]
# get all houses from database
all_houses = houses.find().sort("name", 1).skip(10 * (page - 1)).limit(limit)
return [json.loads(json.dumps(house, default=str)) for house in all_houses]
#### Security ####################################################
async def verify_house_ownership(house_id: str, current_user: User):
await check_database()
houses = learnhouseDB["houses"]
house = houses.find_one({"house_id": house_id})
if not house:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="House does not exist")
if current_user.username not in house["owners"]:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not own this house")
return True
#### Security ####################################################

25
src/services/security.py Normal file
View file

@ -0,0 +1,25 @@
from passlib.context import CryptContext
from jose import JWTError, jwt
from passlib.hash import pbkdf2_sha256
### 🔒 JWT ##############################################################
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
ACCESS_TOKEN_EXPIRE_MINUTES = 30
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
### 🔒 JWT ##############################################################
### 🔒 Passwords Hashing ##############################################################
async def security_hash_password(password: str):
return pbkdf2_sha256.hash(password)
async def security_verify_password(plain_password: str, hashed_password: str):
return pbkdf2_sha256.verify(plain_password, hashed_password)
### 🔒 Passwords Hashing ##############################################################

106
src/services/users.py Normal file
View file

@ -0,0 +1,106 @@
from pydantic import BaseModel
from ..services.database import create_config_collection, check_database, create_database, learnhouseDB, learnhouseDB
from ..services.security import *
from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks
from datetime import datetime
#### Classes ####################################################
class User(BaseModel):
username: str
email: str
full_name: str | None = None
disabled: bool | None = None
avatar_url: str | None = None
verified: bool
created_date: str
bio : str | None = None
class UserInDB(User):
password: str
#### Classes ####################################################
async def get_user(username: str):
check_database()
users = learnhouseDB["users"]
user = users.find_one({"username": username})
if not user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist")
user = User(**user)
return user
async def security_get_user(username: str):
check_database()
users = learnhouseDB["users"]
user = users.find_one({"username": username})
if not user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist")
return UserInDB(**user)
async def update_user(user_object: UserInDB):
check_database()
users = learnhouseDB["users"]
isUserAvailable = users.find_one({"username": user_object.username})
if not isUserAvailable:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist")
user_object.password = security_hash_password(user_object.password)
updated_user = {"$set": user_object.dict()}
users.update_one({"username": user_object.username}, updated_user)
return User(**user_object.dict())
async def delete_user(username: str):
check_database()
users = learnhouseDB["users"]
isUserAvailable = users.find_one({"username": username})
if not isUserAvailable:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist")
users.delete_one({"username": username})
return {"detail": "User deleted"}
async def create_user(user_object: UserInDB):
check_database()
users = learnhouseDB["users"]
isUserAvailable = users.find_one({"username": user_object.username})
if isUserAvailable:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User already exists")
# lowercase username
user_object.username = user_object.username.lower()
user_object.created_date = str(datetime.now())
user_object.password = await security_hash_password(user_object.password)
users.insert_one(user_object.dict())
return User(**user_object.dict())