Working on user, backend.

This commit is contained in:
charles-gauthereau
2024-10-27 11:51:53 +01:00
parent 1069b2c3ad
commit b62261896b
28 changed files with 7351 additions and 3383 deletions
+17
View File
@@ -0,0 +1,17 @@
POSTGRES_SERVER=db
POSTGRES_USER=devuser
POSTGRES_PASSWORD=changeme
POSTGRES_DB=devdb
SMTP_PORT=465
SMTP_SERVER=ssl0.ovh.net
SENDER_MAIL=no-reply@soluce-technologies.com
SENDER_PW=
ALLOWED_HOSTS=["*"]
SECRET_KEY=changeme-secretkey
DEBUG=True
ENVIRONMENT=development
STAGE=True
View File
+12
View File
@@ -0,0 +1,12 @@
from sqlalchemy import Column, DateTime, Integer
from sqlalchemy.sql import func
from database import BaseModel
class Model(BaseModel):
__abstract__ = True
id = Column(Integer, primary_key=True, index=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
+8
View File
@@ -0,0 +1,8 @@
from datetime import datetime
from pydantic import BaseModel as BaseSchema
class Schema(BaseSchema):
id: int | None = None
created_at: datetime | None = None
View File
+24
View File
@@ -0,0 +1,24 @@
from sqlalchemy import asc
from sqlalchemy.orm import Session
from . import models, schemas, services
def get_user(db: Session, user_id: int):
return db.query(models.User).filter(models.User.id == user_id).first()
def get_users(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.User).order_by(asc(models.User.id)).offset(skip).limit(limit).all()
def get_user_by_email(db: Session, email: str):
return db.query(models.User).filter(models.User.email == email).first()
def create_user(db: Session, user: schemas.UserCreate):
fake_hashed_password = services.get_password_hash(user.password)
db_user = models.User(email=user.email, hashed_password=fake_hashed_password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
+13
View File
@@ -0,0 +1,13 @@
from sqlalchemy import Column, Boolean, String, Float
from sqlalchemy.orm import relationship
from apps.base.models import Model
class User(Model):
__tablename__ = 'users'
email = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
+30
View File
@@ -0,0 +1,30 @@
from apps.base.schemas import Schema
class UserBase(Schema):
email: str
class UserCreate(UserBase):
password: str
class User(UserBase):
id: int
is_active: bool
class Config:
from_attributes = True
class Token(Schema):
access_token: str
token_type: str
class TokenData(Schema):
username: str | None = None
class UserInDB(User):
hashed_password: str
+93
View File
@@ -0,0 +1,93 @@
from datetime import datetime, timedelta, timezone
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from apps.users.crud import get_user_by_email
from apps.users.schemas import TokenData, User, UserInDB
from middleware.db_connection import get_db
from settings import config
ALGORITHM = "HS256"
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
}
}
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def authenticate_user(db, username: str, password: str):
user = get_user_by_email(db, username)
if not user:
return False
if not verify_password(password, user.hashed_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.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, config.SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_db)):
print(token)
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[ALGORITHM])
print(payload)
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = get_user_by_email(db, email=token_data.username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(
current_user: Annotated[User, Depends(get_current_user)]
):
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
+18
View File
@@ -0,0 +1,18 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from settings import config
POSTGRES_SERVER = config.POSTGRES_SERVER
POSTGRES_USER = config.POSTGRES_USER
POSTGRES_PASSWORD = config.POSTGRES_PASSWORD
POSTGRES_DB = config.POSTGRES_DB
SQLALCHEMY_DATABASE_URL = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_SERVER}/{POSTGRES_DB}"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
BaseModel = declarative_base()
+22 -17
View File
@@ -1,29 +1,34 @@
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from database import BaseModel, engine
from middleware.db_connection import DatabaseSessionMiddleware
from routes.main import api_router
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
from settings import config
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@asynccontextmanager
async def lifespan(app: FastAPI):
print("Initialising database...")
BaseModel.metadata.create_all(bind=engine)
print("Database initialised!")
yield
print("App shutdown!")
middlewares = []
app = FastAPI(lifespan=lifespan, middleware=middlewares)
app.add_middleware(DatabaseSessionMiddleware)
app.add_middleware(TrustedHostMiddleware, allowed_hosts=config.ALLOWED_HOSTS)
app.include_router(api_router)
if os.getenv("ENV") == "production":
app.mount("/assets", StaticFiles(directory="static/assets"), name="assets")
app.mount("/static", StaticFiles(directory="static", html=True), name="static")
@app.get("/{full_path:path}")
def read_react_app(full_path: str):
return FileResponse("static/index.html")
View File
+20
View File
@@ -0,0 +1,20 @@
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from database import SessionLocal
class DatabaseSessionMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response = Response("Internal server error", status_code=500)
try:
request.state.db = SessionLocal()
response = await call_next(request)
finally:
request.state.db.close()
return response
def get_db(request: Request):
print(request)
return request.state.db
+19
View File
@@ -1,3 +1,10 @@
SQLAlchemy
psycopg2
requests
websockets
python-dotenv
# fastapi libraries
fastapi[all]
fastapi-utilities
@@ -7,3 +14,15 @@ pydantic-settings
# deployment libraries
uvicorn==0.27.0
gunicorn
# cryptographic libraries
python-jose
PyJWT
passlib
paramiko
python-multipart==0.0.12
filetype
biplist
-8
View File
@@ -1,8 +0,0 @@
from fastapi import APIRouter
router = APIRouter(prefix="/api", tags=["api"])
@router.get("/")
async def root():
return {"message": "Hello from backup agent!"}
+48
View File
@@ -0,0 +1,48 @@
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import Annotated
from apps.users.services import authenticate_user, fake_users_db, create_access_token, get_current_active_user
from fastapi.security import OAuth2PasswordRequestForm
from apps.users import crud, models, schemas
from middleware.db_connection import get_db
from apps.users.schemas import User, UserCreate, Token
from settings import config
router = APIRouter(prefix="/users", tags=["users"])
@router.post("/token", response_model=Token)
async def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db: Session = Depends(get_db)):
user = authenticate_user(db, 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=config.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(data={"sub": user.email}, expires_delta=access_token_expires)
return Token(access_token=access_token, token_type="bearer")
@router.get("/list", response_model=list[User])
async def list_users(db: Session = Depends(get_db)):
db_users = crud.get_users(db)
return db_users
@router.get("/me", response_model=User)
async def read_users_me(current_user: Annotated[User, Depends(get_current_active_user)]):
return current_user
@router.post("/", response_model=User)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
db_user = crud.get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return crud.create_user(db=db, user=user)
+8
View File
@@ -0,0 +1,8 @@
from fastapi import APIRouter
router = APIRouter(prefix="/welcome", tags=["api"])
@router.get("/")
async def root():
return {"message": "Hello from portabase!"}
+4 -3
View File
@@ -1,6 +1,7 @@
from fastapi import APIRouter
from routes.http import api
from routes.http import welcome, user
api_router = APIRouter()
api_router.include_router(api.router)
api_router = APIRouter(prefix="/api", tags=["api"])
api_router.include_router(welcome.router)
api_router.include_router(user.router)
+16
View File
@@ -0,0 +1,16 @@
import os
from functools import lru_cache
from settings.development import DevSettings
from settings.production import ProdSettings
@lru_cache
def get_settings():
if os.getenv("ENVIRONMENT") == "development":
return DevSettings()
else:
return ProdSettings()
config = get_settings()
+24
View File
@@ -0,0 +1,24 @@
from typing import List
from pydantic_settings import BaseSettings, SettingsConfigDict
from dotenv import find_dotenv
class Settings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
SMTP_PORT: int
SMTP_SERVER: str
SENDER_MAIL: str
SENDER_PW: str
HTTPS: bool = False
DEBUG: bool = False
ALLOWED_HOSTS: List[str]
SECRET_KEY: str # openssl rand -hex 32
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
STAGE: bool = False
model_config = SettingsConfigDict(env_file=find_dotenv(), extra='allow')
+9
View File
@@ -0,0 +1,9 @@
from typing import List
from settings.base import Settings
class DevSettings(Settings):
ALLOWED_HOSTS: List[str] = ["*"]
SECRET_KEY: str = "changeme-secretkey"
STAGE: bool = True
+6
View File
@@ -0,0 +1,6 @@
from settings.base import Settings
class ProdSettings(Settings):
CRON: bool = True