diff --git a/backend/apps/base/models.py b/backend/apps/base/models.py index 3cd47046..628bd70d 100644 --- a/backend/apps/base/models.py +++ b/backend/apps/base/models.py @@ -1,10 +1,9 @@ from sqlalchemy import Column, DateTime, Integer +from sqlalchemy.orm import DeclarativeBase from sqlalchemy.sql import func -from database import BaseModel - -class Model(BaseModel): +class Model(DeclarativeBase): __abstract__ = True id = Column(Integer, primary_key=True, index=True) diff --git a/backend/apps/users/crud.py b/backend/apps/users/crud.py index 7b6b9612..60c85d59 100644 --- a/backend/apps/users/crud.py +++ b/backend/apps/users/crud.py @@ -1,33 +1,41 @@ -from sqlalchemy import asc -from sqlalchemy.orm import Session -from . import models, schemas, services +from sqlalchemy import asc, select +from sqlalchemy.ext.asyncio import AsyncSession +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): - return db.query(models.User).order_by(asc(models.User.id)).offset(skip).limit(limit).all() +async def get_users(session: AsyncSession, skip: int = 0, limit: int = 100): + 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): - 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 - -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 - - -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 +# +# 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 diff --git a/backend/apps/users/models.py b/backend/apps/users/models.py index 39fd99d9..7197b465 100644 --- a/backend/apps/users/models.py +++ b/backend/apps/users/models.py @@ -1,15 +1,23 @@ -from sqlalchemy import Column, Boolean, String, Float, Enum -from sqlalchemy.orm import relationship - +from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTableUUID +from sqlalchemy.orm import DeclarativeBase from apps.base.models import Model 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) - is_active = Column(Boolean, default=True) - role = Column(Enum(Role), default=Role.USER) # Add a role attribute - # role = Column(String) # Ensure role is defined + +class Base(DeclarativeBase): + pass + + +class User(SQLAlchemyBaseUserTableUUID, Base): + pass diff --git a/backend/apps/users/schemas.py b/backend/apps/users/schemas.py index 12a38555..de1169e4 100644 --- a/backend/apps/users/schemas.py +++ b/backend/apps/users/schemas.py @@ -1,36 +1,15 @@ -from apps.base.schemas import Schema -from apps.users.roles import Role +import uuid + +from fastapi_users import schemas -class UserBase(Schema): - email: str +class UserRead(schemas.BaseUser[uuid.UUID]): + pass -class UserCreate(UserBase): - password: str +class UserCreate(schemas.BaseUserCreate): + pass -class User(UserBase): - id: int - 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 +class UserUpdate(schemas.BaseUserUpdate): + pass diff --git a/backend/apps/users/services.py b/backend/apps/users/services.py index f7780aba..4d34b1ad 100644 --- a/backend/apps/users/services.py +++ b/backend/apps/users/services.py @@ -1,115 +1,56 @@ -from datetime import datetime, timedelta, timezone -from typing import Annotated -from fastapi import Request +import uuid +from typing import Optional -from fastapi import Depends, HTTPException, status, Security -from fastapi.security import OAuth2PasswordBearer -from jose import JWTError, jwt -from passlib.context import CryptContext -from sqlalchemy.orm import Session -from functools import wraps +from fastapi import Depends, Request +from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin +from fastapi_users.authentication import ( + AuthenticationBackend, + BearerTransport, + JWTStrategy, +) +from fastapi_users.db import SQLAlchemyUserDatabase -from apps.users.crud import get_user_by_email -from apps.users.roles import Permission, role_permissions -from apps.users.schemas import TokenData, User, UserInDB -from middleware.db_connection import get_db -from settings import config +from apps.users.models import User +from database import get_user_db -ALGORITHM = "HS256" -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") +SECRET = "SECRET" -def verify_password(plain_password, hashed_password): - return pwd_context.verify(plain_password, hashed_password) +class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): + 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): - return pwd_context.hash(password) +async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)): + yield UserManager(user_db) -def get_user(db, username: str): - if username in db: - user_dict = db[username] - return UserInDB(**user_dict) +bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") -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 get_jwt_strategy() -> JWTStrategy: + return JWTStrategy(secret=SECRET, lifetime_seconds=3600) -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 +auth_backend = AuthenticationBackend( + name="jwt", + transport=bearer_transport, + get_strategy=get_jwt_strategy, +) +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)): - 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 \ No newline at end of file +current_active_user = fastapi_users.current_user(active=True) diff --git a/backend/database.py b/backend/database.py index 7c2d5133..ac3c7672 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1,18 +1,33 @@ -from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker - +from typing import AsyncGenerator +from fastapi import Depends +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 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+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 diff --git a/backend/main.py b/backend/main.py index d28f6268..c1b1b2a4 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,48 +1,18 @@ -import os +import asyncio from contextlib import asynccontextmanager from fastapi import FastAPI -from sqlalchemy.orm import Session -from starlette.middleware.trustedhost import TrustedHostMiddleware - -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 database import create_db_and_tables +from middleware.create_user import create_user 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 async def lifespan(app: FastAPI): - print("Initialising database...") - BaseModel.metadata.create_all(bind=engine) - print("Database initialised!") - # Initialize default users - db = SessionLocal() - try: - init_default_users(db) - finally: - db.close() + await create_db_and_tables() + await create_user("soluce.technologies@gmail.com", "12345678", True) 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 = FastAPI(lifespan=lifespan) app.include_router(api_router) - diff --git a/backend/middleware/create_user.py b/backend/middleware/create_user.py new file mode 100644 index 00000000..ddfa1151 --- /dev/null +++ b/backend/middleware/create_user.py @@ -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 diff --git a/backend/middleware/db_connection.py b/backend/middleware/db_connection.py index f3ebebdd..1b28286b 100644 --- a/backend/middleware/db_connection.py +++ b/backend/middleware/db_connection.py @@ -1,20 +1,26 @@ 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): - 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 +# 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 + diff --git a/backend/requirements/requirements..old.txt b/backend/requirements/requirements..old.txt new file mode 100644 index 00000000..c92c99f2 --- /dev/null +++ b/backend/requirements/requirements..old.txt @@ -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 + + diff --git a/backend/requirements/requirements.txt b/backend/requirements/requirements.txt index 3a7be762..2561f90b 100644 --- a/backend/requirements/requirements.txt +++ b/backend/requirements/requirements.txt @@ -1,28 +1,8 @@ -SQLAlchemy -psycopg2 - -requests -websockets -python-dotenv - -# fastapi libraries -fastapi[all] -fastapi-utilities -pydantic==2.6.0 -pydantic-settings -alembic -# deployment libraries -uvicorn==0.27.0 +fastapi +fastapi-users[sqlalchemy] +uvicorn[standard] +aiosqlite gunicorn - - -# cryptographic libraries -python-jose -PyJWT -passlib -paramiko - - -python-multipart==0.0.12 -filetype -biplist \ No newline at end of file +pydantic-settings +asyncpg +psycopg2 \ No newline at end of file diff --git a/backend/routes/http/agent.py b/backend/routes/http/agent.py index df73a53e..b6c7add4 100644 --- a/backend/routes/http/agent.py +++ b/backend/routes/http/agent.py @@ -1,20 +1,20 @@ -from fastapi import Depends, APIRouter, Request -from sqlalchemy.orm import Session -from apps.users.roles import Permission -from apps.users.services import permission_required, get_current_user, get_current_active_user -from pydantic import BaseModel -from middleware.db_connection import get_db - - -router = APIRouter(prefix="/agents", tags=["agents"]) - - -class Agent(BaseModel): - name: str - - -@router.post("/", dependencies=[Depends(get_current_active_user)]) -@permission_required(Permission.CREATE, Permission.READ) -async def create_agent(agent: Agent, request: Request, db: Session = Depends(get_db)): - print(agent) - return True +# from fastapi import Depends, APIRouter, Request +# from sqlalchemy.orm import Session +# from apps.users.roles import Permission +# from apps.users.services import permission_required, get_current_user, get_current_active_user +# from pydantic import BaseModel +# from middleware.db_connection import get_db +# +# +# router = APIRouter(prefix="/agents", tags=["agents"]) +# +# +# class Agent(BaseModel): +# name: str +# +# +# @router.post("/", dependencies=[Depends(get_current_active_user)]) +# @permission_required(Permission.CREATE, Permission.READ) +# async def create_agent(agent: Agent, request: Request, db: Session = Depends(get_db)): +# print(agent) +# return True diff --git a/backend/routes/http/auth.py b/backend/routes/http/auth.py new file mode 100644 index 00000000..99a2dab7 --- /dev/null +++ b/backend/routes/http/auth.py @@ -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"], +) \ No newline at end of file diff --git a/backend/routes/http/user.py b/backend/routes/http/user.py index 5449415d..694ec1df 100644 --- a/backend/routes/http/user.py +++ b/backend/routes/http/user.py @@ -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 typing import Annotated -from apps.users.roles import Permission -from apps.users.services import authenticate_user, create_access_token, get_current_active_user -from fastapi.security import OAuth2PasswordRequestForm - -from apps.users import crud, models, schemas +from apps.users import crud +from apps.users.models import User +from apps.users.schemas import UserRead, UserUpdate +from apps.users.services import fastapi_users, current_active_user +from database import get_user_db, get_async_session from middleware.db_connection import get_db +from sqlalchemy.future import select -from apps.users.schemas import User, UserCreate, Token -from settings import config - +# router = APIRouter() 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") +current_superuser = fastapi_users.current_user(active=True, superuser=True) -@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("/list", response_model=list[BaseUser]) +# async def list_users(db: Session = Depends(get_db), user: User = Depends(current_superuser)): +# 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.get("/list", response_model=List[BaseUser]) +async def get_users(skip: int = 0, limit: int = 10, session: AsyncSession = Depends(get_async_session)): + users = await crud.get_users(session=session, skip=skip, limit=limit) - -@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) + return users +router.include_router( + fastapi_users.get_users_router(UserRead, UserUpdate) +) diff --git a/backend/routes/main.py b/backend/routes/main.py index 17bfe408..10d50213 100644 --- a/backend/routes/main.py +++ b/backend/routes/main.py @@ -1,8 +1,9 @@ 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.include_router(welcome.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)