diff --git a/Makefile b/Makefile index 13097909..3a696433 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,14 @@ start-dev: docker compose -f docker/compose/docker-compose.yml up +.PHONY: down dev volumes +down-dev-volumes: + docker compose -f docker/compose/docker-compose.yml down --volumes + + + + + .PHONY: build dev build-dev: docker compose -f docker/compose/docker-compose.yml build diff --git a/backend/apps/agents/models.py b/backend/apps/agents/models.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/users/crud.py b/backend/apps/users/crud.py index 059eeee3..7b6b9612 100644 --- a/backend/apps/users/crud.py +++ b/backend/apps/users/crud.py @@ -22,3 +22,12 @@ def create_user(db: Session, user: schemas.UserCreate): 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 a32b88c6..39fd99d9 100644 --- a/backend/apps/users/models.py +++ b/backend/apps/users/models.py @@ -1,7 +1,8 @@ -from sqlalchemy import Column, Boolean, String, Float +from sqlalchemy import Column, Boolean, String, Float, Enum from sqlalchemy.orm import relationship from apps.base.models import Model +from apps.users.roles import Role class User(Model): @@ -10,4 +11,5 @@ class User(Model): 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 diff --git a/backend/apps/users/roles.py b/backend/apps/users/roles.py new file mode 100644 index 00000000..304cfb88 --- /dev/null +++ b/backend/apps/users/roles.py @@ -0,0 +1,21 @@ +from enum import Enum + + +class Role(str, Enum): + ADMIN = "admin" + MODERATOR = "moderator" + USER = "user" + + +class Permission(str, Enum): + CREATE = "create" + READ = "read" + UPDATE = "update" + DELETE = "delete" + + +role_permissions = { + Role.ADMIN: [Permission.CREATE, Permission.READ, Permission.UPDATE, Permission.DELETE], + Role.MODERATOR: [Permission.CREATE, Permission.READ, Permission.UPDATE], + Role.USER: [Permission.READ] +} diff --git a/backend/apps/users/schemas.py b/backend/apps/users/schemas.py index ae06c589..12a38555 100644 --- a/backend/apps/users/schemas.py +++ b/backend/apps/users/schemas.py @@ -1,4 +1,5 @@ from apps.base.schemas import Schema +from apps.users.roles import Role class UserBase(Schema): @@ -12,6 +13,7 @@ class UserCreate(UserBase): class User(UserBase): id: int is_active: bool + role: Role class Config: from_attributes = True @@ -28,3 +30,7 @@ class TokenData(Schema): class UserInDB(User): hashed_password: str + + +class AdminUserCreate(UserCreate): + role: Role diff --git a/backend/apps/users/services.py b/backend/apps/users/services.py index b3852b56..5df8cd6c 100644 --- a/backend/apps/users/services.py +++ b/backend/apps/users/services.py @@ -1,13 +1,16 @@ from datetime import datetime, timedelta, timezone from typing import Annotated +from fastapi import Request -from fastapi import Depends, HTTPException, status +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 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 @@ -64,22 +67,24 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None): 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: + print(token) payload = jwt.decode(token, config.SECRET_KEY, algorithms=[ALGORITHM]) - print(payload) + 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 @@ -91,3 +96,44 @@ async def get_current_active_user( if not current_user.is_active: raise HTTPException(status_code=400, detail="Inactive user") return current_user + + +def permission_required(permission: Permission): + def decorator(func): + @wraps(func) + async def decorated_function(*args, **kwargs): + print("test", 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", + ) + + # Get the authorization header + authorization_header = request.headers.get("Authorization") + + # Extract the token from the authorization header + if not authorization_header: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Missing authorization header", + ) + + # Assuming the token is in the format "Bearer " + token = authorization_header.split(" ")[1] + + current_user = await get_current_user(token=token, db=db) + # Check if the user has the required permission + if permission not in role_permissions[current_user.role]: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You do not have the required permission", + ) + # Call the decorated function with the original arguments + return await func(*args, **kwargs) + + return decorated_function + + return decorator diff --git a/backend/main.py b/backend/main.py index 5f7de7e4..d28f6268 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,24 +1,38 @@ import os from contextlib import asynccontextmanager - from fastapi import FastAPI +from sqlalchemy.orm import Session from starlette.middleware.trustedhost import TrustedHostMiddleware -from starlette.staticfiles import StaticFiles -from fastapi.responses import FileResponse -from database import BaseModel, engine + +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 fastapi.middleware.cors import CORSMiddleware - 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() + yield print("App shutdown!") @@ -27,8 +41,8 @@ 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) + diff --git a/backend/requirements/requirements.txt b/backend/requirements/requirements.txt index 26d352af..3a7be762 100644 --- a/backend/requirements/requirements.txt +++ b/backend/requirements/requirements.txt @@ -10,7 +10,7 @@ fastapi[all] fastapi-utilities pydantic==2.6.0 pydantic-settings - +alembic # deployment libraries uvicorn==0.27.0 gunicorn diff --git a/backend/routes/http/agent.py b/backend/routes/http/agent.py new file mode 100644 index 00000000..c0d1b9b9 --- /dev/null +++ b/backend/routes/http/agent.py @@ -0,0 +1,22 @@ +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 + + +# Example usage: +@router.post("/", dependencies=[Depends(get_current_active_user)]) +@permission_required(Permission.CREATE) +async def create_agent(agent: Agent, request: Request, db: Session = Depends(get_db)): + print(agent) + return True diff --git a/backend/routes/http/user.py b/backend/routes/http/user.py index e27c61b1..0978ea74 100644 --- a/backend/routes/http/user.py +++ b/backend/routes/http/user.py @@ -3,6 +3,8 @@ from datetime import timedelta from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from typing import Annotated + +from apps.users.roles import Permission from apps.users.services import authenticate_user, fake_users_db, create_access_token, get_current_active_user from fastapi.security import OAuth2PasswordRequestForm @@ -16,7 +18,8 @@ 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)): +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( @@ -46,3 +49,5 @@ def create_user(user: UserCreate, db: Session = Depends(get_db)): if db_user: raise HTTPException(status_code=400, detail="Email already registered") return crud.create_user(db=db, user=user) + + diff --git a/backend/routes/main.py b/backend/routes/main.py index 7b6a1cb5..17bfe408 100644 --- a/backend/routes/main.py +++ b/backend/routes/main.py @@ -1,7 +1,8 @@ from fastapi import APIRouter -from routes.http import welcome, user +from routes.http import welcome, user, agent 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)