Working on the implementation of fastapi users.

This commit is contained in:
charles-gauthereau
2024-11-02 12:38:05 +01:00
parent ff7f864ec2
commit 54248b8670
15 changed files with 289 additions and 312 deletions
+2 -3
View File
@@ -1,10 +1,9 @@
from sqlalchemy import Column, DateTime, Integer from sqlalchemy import Column, DateTime, Integer
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.sql import func from sqlalchemy.sql import func
from database import BaseModel
class Model(DeclarativeBase):
class Model(BaseModel):
__abstract__ = True __abstract__ = True
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
+34 -26
View File
@@ -1,33 +1,41 @@
from sqlalchemy import asc from sqlalchemy import asc, select
from sqlalchemy.orm import Session from sqlalchemy.ext.asyncio import AsyncSession
from . import models, schemas, services from .models import User
def get_user(db: Session, user_id: int): #
return db.query(models.User).filter(models.User.id == user_id).first() # 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_users(db: Session, skip: int = 0, limit: int = 100): async def get_users(session: AsyncSession, skip: int = 0, limit: int = 100):
return db.query(models.User).order_by(asc(models.User.id)).offset(skip).limit(limit).all() query = select(User).order_by(asc(User.id)).offset(skip).limit(limit)
result = await session.execute(query)
users = result.scalars().all()
return users
#
# def get_user_by_email(db: Session, email: str):
# return db.query(models.User).filter(models.User.email == email).first()
def get_user_by_email(db: Session, email: str): # def create_user(db: Session, user: schemas.UserCreate):
return db.query(models.User).filter(models.User.email == email).first() # 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
#
def create_user(db: Session, user: schemas.UserCreate): # def create_admin_user(db: Session, user: schemas.AdminUserCreate):
fake_hashed_password = services.get_password_hash(user.password) # fake_hashed_password = services.get_password_hash(user.password)
db_user = models.User(email=user.email, hashed_password=fake_hashed_password) # db_user = models.User(email=user.email, hashed_password=fake_hashed_password, role=user.role)
db.add(db_user) # db.add(db_user)
db.commit() # db.commit()
db.refresh(db_user) # db.refresh(db_user)
return db_user # return db_user
def create_admin_user(db: Session, user: schemas.AdminUserCreate):
fake_hashed_password = services.get_password_hash(user.password)
db_user = models.User(email=user.email, hashed_password=fake_hashed_password, role=user.role)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
+18 -10
View File
@@ -1,15 +1,23 @@
from sqlalchemy import Column, Boolean, String, Float, Enum from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTableUUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import DeclarativeBase
from apps.base.models import Model from apps.base.models import Model
from apps.users.roles import Role from apps.users.roles import Role
class User(Model): #
__tablename__ = 'users' # class User(Model):
# __tablename__ = 'users'
#
# email = Column(String, unique=True, index=True)
# hashed_password = Column(String)
# is_active = Column(Boolean, default=True)
# role = Column(Enum(Role), default=Role.USER) # Add a role attribute
# # role = Column(String) # Ensure role is defined
email = Column(String, unique=True, index=True)
hashed_password = Column(String) class Base(DeclarativeBase):
is_active = Column(Boolean, default=True) pass
role = Column(Enum(Role), default=Role.USER) # Add a role attribute
# role = Column(String) # Ensure role is defined
class User(SQLAlchemyBaseUserTableUUID, Base):
pass
+9 -30
View File
@@ -1,36 +1,15 @@
from apps.base.schemas import Schema import uuid
from apps.users.roles import Role
from fastapi_users import schemas
class UserBase(Schema): class UserRead(schemas.BaseUser[uuid.UUID]):
email: str pass
class UserCreate(UserBase): class UserCreate(schemas.BaseUserCreate):
password: str pass
class User(UserBase): class UserUpdate(schemas.BaseUserUpdate):
id: int pass
is_active: bool
role: Role
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
class AdminUserCreate(UserCreate):
role: Role
+41 -100
View File
@@ -1,115 +1,56 @@
from datetime import datetime, timedelta, timezone import uuid
from typing import Annotated from typing import Optional
from fastapi import Request
from fastapi import Depends, HTTPException, status, Security from fastapi import Depends, Request
from fastapi.security import OAuth2PasswordBearer from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin
from jose import JWTError, jwt from fastapi_users.authentication import (
from passlib.context import CryptContext AuthenticationBackend,
from sqlalchemy.orm import Session BearerTransport,
from functools import wraps JWTStrategy,
)
from fastapi_users.db import SQLAlchemyUserDatabase
from apps.users.crud import get_user_by_email from apps.users.models import User
from apps.users.roles import Permission, role_permissions from database import get_user_db
from apps.users.schemas import TokenData, User, UserInDB
from middleware.db_connection import get_db
from settings import config
ALGORITHM = "HS256" SECRET = "SECRET"
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def verify_password(plain_password, hashed_password): class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
return pwd_context.verify(plain_password, hashed_password) reset_password_token_secret = SECRET
verification_token_secret = SECRET
async def on_after_register(self, user: User, request: Optional[Request] = None):
print(f"User {user.id} has registered.")
async def on_after_forgot_password(
self, user: User, token: str, request: Optional[Request] = None
):
print(f"User {user.id} has forgot their password. Reset token: {token}")
async def on_after_request_verify(
self, user: User, token: str, request: Optional[Request] = None
):
print(f"Verification requested for user {user.id}. Verification token: {token}")
def get_password_hash(password): async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
return pwd_context.hash(password) yield UserManager(user_db)
def get_user(db, username: str): bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def authenticate_user(db, username: str, password: str): def get_jwt_strategy() -> JWTStrategy:
user = get_user_by_email(db, username) return JWTStrategy(secret=SECRET, lifetime_seconds=3600)
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): auth_backend = AuthenticationBackend(
to_encode = data.copy() name="jwt",
if expires_delta: transport=bearer_transport,
expire = datetime.now(timezone.utc) + expires_delta get_strategy=get_jwt_strategy,
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
fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [auth_backend])
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_db)): current_active_user = fastapi_users.current_user(active=True)
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
print(token)
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[ALGORITHM])
print("payload", payload)
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
print(token_data.username)
user = get_user_by_email(db, email=token_data.username)
print(user)
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
def permission_required(*permissions: Permission):
def decorator(func):
@wraps(func)
async def decorated_function(*args, **kwargs):
request: Request = kwargs.get("request")
db: Session = kwargs.get("db")
if not request:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Missing request object",
)
authorization_header = request.headers.get("Authorization")
if not authorization_header:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing authorization header",
)
token = authorization_header.split(" ")[1]
current_user = await get_current_user(token=token, db=db)
for permission in permissions:
if permission not in role_permissions[current_user.role]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"You do not have the required permission: {permission.value}",
)
return await func(*args, **kwargs)
return decorated_function
return decorator
+23 -8
View File
@@ -1,18 +1,33 @@
from sqlalchemy import create_engine from typing import AsyncGenerator
from sqlalchemy.ext.declarative import declarative_base from fastapi import Depends
from sqlalchemy.orm import sessionmaker from fastapi_users.db import SQLAlchemyUserDatabase
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine, async_session
from apps.users.models import Base, User
from settings import config from settings import config
POSTGRES_SERVER = config.POSTGRES_SERVER POSTGRES_SERVER = config.POSTGRES_SERVER
POSTGRES_USER = config.POSTGRES_USER POSTGRES_USER = config.POSTGRES_USER
POSTGRES_PASSWORD = config.POSTGRES_PASSWORD POSTGRES_PASSWORD = config.POSTGRES_PASSWORD
POSTGRES_DB = config.POSTGRES_DB POSTGRES_DB = config.POSTGRES_DB
SQLALCHEMY_DATABASE_URL = f"postgresql+asyncpg://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_SERVER}/{POSTGRES_DB}"
engine = create_async_engine(SQLALCHEMY_DATABASE_URL)
async_session_maker = async_sessionmaker(engine, expire_on_commit=False)
SQLALCHEMY_DATABASE_URL = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_SERVER}/{POSTGRES_DB}"
engine = create_engine(SQLALCHEMY_DATABASE_URL) async def create_db_and_tables():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
BaseModel = declarative_base() async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
async with async_session_maker() as session:
yield session
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
yield SQLAlchemyUserDatabase(session, User)
async def get_db_session() -> AsyncSession:
async with async_session() as session:
yield session
+6 -36
View File
@@ -1,48 +1,18 @@
import os import asyncio
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from sqlalchemy.orm import Session from database import create_db_and_tables
from starlette.middleware.trustedhost import TrustedHostMiddleware from middleware.create_user import create_user
from apps.users.roles import Role
from apps.users.schemas import UserCreate, User, AdminUserCreate
from database import BaseModel, engine, SessionLocal
from middleware.db_connection import DatabaseSessionMiddleware
from routes.main import api_router from routes.main import api_router
from settings import config
def init_default_users(db: Session):
from apps.users import crud
print("Initializing default users...")
if not crud.get_user_by_email(db, "admin@portabase.com"):
print("Creating admin user...")
user = AdminUserCreate(email="admin@portabase.com", password="password", role=Role.ADMIN)
crud.create_admin_user(db, user)
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
print("Initialising database...") await create_db_and_tables()
BaseModel.metadata.create_all(bind=engine) await create_user("soluce.technologies@gmail.com", "12345678", True)
print("Database initialised!")
# Initialize default users
db = SessionLocal()
try:
init_default_users(db)
finally:
db.close()
yield yield
print("App shutdown!")
middlewares = [] app = FastAPI(lifespan=lifespan)
app = FastAPI(lifespan=lifespan, middleware=middlewares)
app.add_middleware(DatabaseSessionMiddleware)
app.add_middleware(TrustedHostMiddleware, allowed_hosts=config.ALLOWED_HOSTS)
app.include_router(api_router) app.include_router(api_router)
+28
View File
@@ -0,0 +1,28 @@
import contextlib
from fastapi_users.exceptions import UserAlreadyExists
from apps.users.schemas import UserCreate
from apps.users.services import get_user_manager
from database import get_async_session, get_user_db
get_async_session_context = contextlib.asynccontextmanager(get_async_session)
get_user_db_context = contextlib.asynccontextmanager(get_user_db)
get_user_manager_context = contextlib.asynccontextmanager(get_user_manager)
async def create_user(email: str, password: str, is_superuser: bool = False):
try:
async with get_async_session_context() as session:
async with get_user_db_context(session) as user_db:
async with get_user_manager_context(user_db) as user_manager:
user = await user_manager.create(
UserCreate(
email=email, password=password, is_superuser=is_superuser
)
)
print(f"User created {user}")
return user
except UserAlreadyExists:
print(f"User {email} already exists")
# raise
+17 -11
View File
@@ -1,20 +1,26 @@
from fastapi import Request, Response from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware from sqlalchemy.ext.asyncio import AsyncSession
from database import SessionLocal from database import get_async_session
class DatabaseSessionMiddleware(BaseHTTPMiddleware): # from starlette.middleware.base import BaseHTTPMiddleware
async def dispatch(self, request: Request, call_next):
response = Response("Internal server error", status_code=500) # from database import SessionLocal
try:
request.state.db = SessionLocal() #
response = await call_next(request) # class DatabaseSessionMiddleware(BaseHTTPMiddleware):
finally: # async def dispatch(self, request: Request, call_next):
request.state.db.close() # response = Response("Internal server error", status_code=500)
return response # try:
# request.state.db = SessionLocal()
# response = await call_next(request)
# finally:
# request.state.db.close()
# return response
def get_db(request: Request): def get_db(request: Request):
print(request) print(request)
return request.state.db return request.state.db
@@ -0,0 +1,31 @@
#SQLAlchemy
#psycopg2
fastapi-users[sqlalchemy]
requests
websockets
python-dotenv
# fastapi libraries
fastapi[all]
fastapi-utilities
pydantic==2.6.0
pydantic-settings
alembic
# deployment libraries
uvicorn==0.27.0
gunicorn
# cryptographic libraries
python-jose
PyJWT
passlib
paramiko
python-multipart==0.0.12
filetype
biplist
+7 -27
View File
@@ -1,28 +1,8 @@
SQLAlchemy fastapi
psycopg2 fastapi-users[sqlalchemy]
uvicorn[standard]
requests aiosqlite
websockets
python-dotenv
# fastapi libraries
fastapi[all]
fastapi-utilities
pydantic==2.6.0
pydantic-settings
alembic
# deployment libraries
uvicorn==0.27.0
gunicorn gunicorn
pydantic-settings
asyncpg
# cryptographic libraries psycopg2
python-jose
PyJWT
passlib
paramiko
python-multipart==0.0.12
filetype
biplist
+20 -20
View File
@@ -1,20 +1,20 @@
from fastapi import Depends, APIRouter, Request # from fastapi import Depends, APIRouter, Request
from sqlalchemy.orm import Session # from sqlalchemy.orm import Session
from apps.users.roles import Permission # from apps.users.roles import Permission
from apps.users.services import permission_required, get_current_user, get_current_active_user # from apps.users.services import permission_required, get_current_user, get_current_active_user
from pydantic import BaseModel # from pydantic import BaseModel
from middleware.db_connection import get_db # from middleware.db_connection import get_db
#
#
router = APIRouter(prefix="/agents", tags=["agents"]) # router = APIRouter(prefix="/agents", tags=["agents"])
#
#
class Agent(BaseModel): # class Agent(BaseModel):
name: str # name: str
#
#
@router.post("/", dependencies=[Depends(get_current_active_user)]) # @router.post("/", dependencies=[Depends(get_current_active_user)])
@permission_required(Permission.CREATE, Permission.READ) # @permission_required(Permission.CREATE, Permission.READ)
async def create_agent(agent: Agent, request: Request, db: Session = Depends(get_db)): # async def create_agent(agent: Agent, request: Request, db: Session = Depends(get_db)):
print(agent) # print(agent)
return True # return True
+25
View File
@@ -0,0 +1,25 @@
from fastapi import APIRouter
from apps.users.schemas import UserRead, UserCreate
from apps.users.services import fastapi_users, auth_backend
router = APIRouter()
router.include_router(
fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"]
)
router.include_router(
fastapi_users.get_register_router(UserRead, UserCreate),
prefix="/auth",
tags=["auth"],
)
router.include_router(
fastapi_users.get_reset_password_router(),
prefix="/auth",
tags=["auth"],
)
router.include_router(
fastapi_users.get_verify_router(UserRead),
prefix="/auth",
tags=["auth"],
)
+25 -39
View File
@@ -1,53 +1,39 @@
from datetime import timedelta from typing import List
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException
from fastapi_users import FastAPIUsers
from fastapi_users.schemas import BaseUser
from sqlalchemy import asc
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import Annotated
from apps.users.roles import Permission from apps.users import crud
from apps.users.services import authenticate_user, create_access_token, get_current_active_user from apps.users.models import User
from fastapi.security import OAuth2PasswordRequestForm from apps.users.schemas import UserRead, UserUpdate
from apps.users.services import fastapi_users, current_active_user
from apps.users import crud, models, schemas from database import get_user_db, get_async_session
from middleware.db_connection import get_db from middleware.db_connection import get_db
from sqlalchemy.future import select
from apps.users.schemas import User, UserCreate, Token # router = APIRouter()
from settings import config
router = APIRouter(prefix="/users", tags=["users"]) router = APIRouter(prefix="/users", tags=["users"])
current_superuser = fastapi_users.current_user(active=True, superuser=True)
@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]) # @router.get("/list", response_model=list[BaseUser])
async def list_users(db: Session = Depends(get_db)): # async def list_users(db: Session = Depends(get_db), user: User = Depends(current_superuser)):
db_users = crud.get_users(db) # db_users = crud.get_users(db)
return db_users # return db_users
@router.get("/me", response_model=User) @router.get("/list", response_model=List[BaseUser])
async def read_users_me(current_user: Annotated[User, Depends(get_current_active_user)]): async def get_users(skip: int = 0, limit: int = 10, session: AsyncSession = Depends(get_async_session)):
return current_user users = await crud.get_users(session=session, skip=skip, limit=limit)
return users
@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)
router.include_router(
fastapi_users.get_users_router(UserRead, UserUpdate)
)
+3 -2
View File
@@ -1,8 +1,9 @@
from fastapi import APIRouter from fastapi import APIRouter
from routes.http import welcome, user, agent from routes.http import welcome, user, agent, auth
api_router = APIRouter(prefix="/api", tags=["api"]) api_router = APIRouter(prefix="/api", tags=["api"])
api_router.include_router(welcome.router) api_router.include_router(welcome.router)
api_router.include_router(user.router) api_router.include_router(user.router)
api_router.include_router(agent.router) api_router.include_router(auth.router)
# api_router.include_router(agent.router)