diff --git a/README.md b/README.md index e6829e6..d0a23ab 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,10 @@ A multi-tenant SaaS backend built with FastAPI, PostgreSQL, and SQLAlchemy. ## Prerequisites +- Python 3.12+ +- PostgreSQL 12+ - Python 3.10+ - PostgreSQL 12+ -- Node.js (for npm scripts) ## Getting Started @@ -44,7 +45,7 @@ python -m venv venv ```bash pip install -r requirements.txt -npm install # For cross-env support in npm scripts +pip install -r requirements.txt ``` ### 4. Environment Configuration @@ -112,11 +113,11 @@ alembic revision --autogenerate -m "Initial schema" #### Run Migrations ```bash -# Using npm scripts (recommended - handles APP_ENV automatically) -npm run migrate:local -npm run migrate:dev -npm run migrate:prod -npm run migrate:test +# Using manage.py (recommended - handles APP_ENV automatically) +python manage.py migrate --env local +python manage.py migrate --env development +python manage.py migrate --env production +python manage.py migrate --env testing # Or using alembic directly $env:APP_ENV="local" # Set environment first @@ -148,20 +149,20 @@ After running migrations, seed the database with initial data: ```bash # Seed super admin user -npm run seed:superadmin:local +python manage.py seed superadmin --env local # Seed default color palettes -npm run seed:palettes:local +python manage.py seed palettes --env local ``` ### 7. Run the Application ```bash -# Using npm scripts (recommended) -npm run local # Local environment -npm run dev # Development environment -npm run prod # Production environment -npm run test # Testing environment +# Using manage.py (recommended) +python manage.py run --env local # Local environment +python manage.py run --env development # Development environment +python manage.py run --env production # Production environment +python manage.py run --env testing # Testing environment # Or using Python directly python run.py @@ -209,7 +210,8 @@ backend/ │ ├── seed_palettes.py │ └── seed_superadmin.py ├── alembic.ini # Alembic configuration -├── package.json # NPM scripts +├── alembic.ini # Alembic configuration +├── manage.py # Management CLI script ├── requirements.txt # Python dependencies └── run.py # Application entry point ``` diff --git a/app/__init__.py b/app/__init__.py index 0d82ab2..9d11048 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -5,21 +5,21 @@ import logging from sqlalchemy import text from app.config.settings import settings from app.config.database import engine - -# Import models for Alembic import app.models.auth.user_model import app.models.auth.role_model import app.models.auth.tenant_model import app.models.theme.color_palette_model - -# New Module Integration Models import app.models.auth.module_model import app.models.auth.module_environment_model import app.models.auth.tenant_module_model import app.models.auth.sso_grant_model -import app.models.auth.access_model # Re-import to ensure updates are picked up +import app.models.auth.access_model import app.models.system.event_log_model - +import asyncio +from app.services.auth.event_service import EventService +from app.config.database import SessionLocal +from app.core.redis import redis_client, sync_redis_client +from fastapi.concurrency import run_in_threadpool # Configure logging logging.basicConfig( @@ -141,11 +141,6 @@ def create_app() -> FastAPI: f"{settings.PROJECT_NAME} v{settings.VERSION} started ({settings.APP_ENV})" ) - import asyncio - from app.services.auth.event_service import EventService - from app.config.database import SessionLocal - from app.core.redis import redis_client, sync_redis_client - from fastapi.concurrency import run_in_threadpool async def redis_event_consumer(): logger.info("Redis Event Consumer STARTED") @@ -185,7 +180,6 @@ def create_app() -> FastAPI: @app.on_event("shutdown") async def shutdown_event(): logger.info("Shutting down...") - from app.core.redis import redis_client await redis_client.close() diff --git a/app/controllers/auth/user_controller.py b/app/controllers/auth/user_controller.py index 664d63a..6d57974 100644 --- a/app/controllers/auth/user_controller.py +++ b/app/controllers/auth/user_controller.py @@ -1,6 +1,5 @@ from sqlalchemy.orm import Session from fastapi import HTTPException, status, BackgroundTasks -from fastapi import HTTPException, status from typing import List, Optional import uuid from app.models.auth.user_model import User diff --git a/app/controllers/theme/color_palette_controller.py b/app/controllers/theme/color_palette_controller.py index f910c90..c8c56b5 100644 --- a/app/controllers/theme/color_palette_controller.py +++ b/app/controllers/theme/color_palette_controller.py @@ -9,7 +9,6 @@ from app.schemas.theme.color_palette_schema import ( ) from app.services.theme.color_palette_service import PaletteService - class PaletteController: @staticmethod def get_all_palettes(db: Session, current_user: User) -> List[ColorPalette]: @@ -35,4 +34,4 @@ class PaletteController: @staticmethod def delete_palette(db: Session, palette_id: UUID): - return PaletteService.delete_palette(db, palette_id) + return PaletteService.delete_palette(db, palette_id) \ No newline at end of file diff --git a/app/routes/auth/__init__.py b/app/routes/auth/__init__.py index c350d28..693ec4f 100644 --- a/app/routes/auth/__init__.py +++ b/app/routes/auth/__init__.py @@ -1,3 +1,4 @@ from .auth import router from .tenant import router -from .role import router \ No newline at end of file +from .role import router +from .user import router \ No newline at end of file diff --git a/app/routes/theme/__init__.py b/app/routes/theme/__init__.py index e69de29..b24e28e 100644 --- a/app/routes/theme/__init__.py +++ b/app/routes/theme/__init__.py @@ -0,0 +1 @@ +from .color_palette import router \ No newline at end of file diff --git a/app/routes/theme/color_palette.py b/app/routes/theme/color_palette.py index 5514d43..1f48bc3 100644 --- a/app/routes/theme/color_palette.py +++ b/app/routes/theme/color_palette.py @@ -14,14 +14,12 @@ from app.models.auth.user_model import User router = APIRouter() - @router.get("/get", response_model=List[ColorPaletteResponse]) def get_all_palettes( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): return PaletteController.get_all_palettes(db, current_user) - @router.get("/get/{palette_id}", response_model=ColorPaletteResponse) def get_palette( palette_id: UUID, @@ -30,7 +28,6 @@ def get_palette( ): return PaletteController.get_palette(db, palette_id) - @router.post( "/create", response_model=ColorPaletteResponse, status_code=status.HTTP_201_CREATED ) @@ -42,12 +39,10 @@ def create_palette( ): return PaletteController.create_palette(db, data, current_user) - @router.put("/update/users/me/preferences", deprecated=True) def update_user_preference(): pass - @router.put("/update/{palette_id}", response_model=ColorPaletteResponse) def update_palette( palette_id: UUID, @@ -58,7 +53,6 @@ def update_palette( ): return PaletteController.update_palette(db, palette_id, data) - @router.delete("/delete/{palette_id}", status_code=status.HTTP_200_OK) def delete_palette( palette_id: UUID, @@ -66,4 +60,4 @@ def delete_palette( current_user: User = Depends(get_current_user), _=Depends(require_access("superadmin.palette.delete")), ): - return PaletteController.delete_palette(db, palette_id) + return PaletteController.delete_palette(db, palette_id) \ No newline at end of file diff --git a/app/services/auth/tenant_service.py b/app/services/auth/tenant_service.py index efdea34..bac24f3 100644 --- a/app/services/auth/tenant_service.py +++ b/app/services/auth/tenant_service.py @@ -173,7 +173,7 @@ class TenantService: if event_targets: payload = { "tenant_id": str(tenant.id), - "tenant_name": tenant.tenant_name, # Note: using current name (might be old if not updated yet, but usually distinct requests) + "tenant_name": tenant.tenant_name, "provisioning_id": provisioning_id, "targets": event_targets } diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..d1da93b --- /dev/null +++ b/manage.py @@ -0,0 +1,58 @@ +import os +import subprocess +import sys +import typer +from typing import Optional + +app = typer.Typer(help="Management script for SaaS Backend") +seed_app = typer.Typer(help="Seeding commands") +app.add_typer(seed_app, name="seed") + +def run_command(args: list[str], env_name: str): + """ + Helper to run commands with a specific APP_ENV. + Replaces "python" with sys.executable to ensure the same interpreter is used. + """ + env = os.environ.copy() + env["APP_ENV"] = env_name + + if args[0] == "python": + args[0] = sys.executable + + try: + subprocess.run(args, env=env, check=True) + except subprocess.CalledProcessError as e: + sys.exit(e.returncode) + except KeyboardInterrupt: + sys.exit(0) + +@app.command() +def run( + env: str = typer.Option("local", "--env", "-e", help="Environment to run in (local, development, production, testing)") +): + """Start the FastAPI server.""" + run_command(["python", "run.py"], env) + +@app.command() +def migrate( + env: str = typer.Option("local", "--env", "-e", help="Environment to run in (local, development, production, testing)") +): + """Run Alembic migrations (upgrade head).""" + run_command(["python", "-m", "alembic", "upgrade", "head"], env) + +@seed_app.command("palettes") +def seed_palettes( + env: str = typer.Option("local", "--env", "-e", help="Environment to run in (local, development, production, testing)") +): + """Seed palettes database.""" + run_command(["python", "scripts/seed_palettes.py"], env) + +@seed_app.command("superadmin") +def seed_superadmin( + env: str = typer.Option("local", "--env", "-e", help="Environment to run in (local, development, production, testing)") +): + """Seed superadmin user.""" + run_command(["python", "scripts/seed_superadmin.py"], env) + +if __name__ == "__main__": + app() \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 19fbad1..0000000 --- a/package-lock.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "name": "saas-backend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "saas-backend", - "version": "1.0.0", - "devDependencies": { - "cross-env": "^7.0.3" - } - }, - "node_modules/cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "bin": { - "cross-env": "src/bin/cross-env.js", - "cross-env-shell": "src/bin/cross-env-shell.js" - }, - "engines": { - "node": ">=10.14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 13e0c15..0000000 --- a/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "saas-backend", - "version": "1.0.0", - "description": "Backend for SaaS architecture", - "main": "run.py", - "scripts": { - "start": "python run.py", - "local": "cross-env APP_ENV=local python run.py", - "dev": "cross-env APP_ENV=development python run.py", - "prod": "cross-env APP_ENV=production python run.py", - "test": "cross-env APP_ENV=testing python run.py", - "migrate:local": "cross-env APP_ENV=local python -m alembic upgrade head", - "migrate:dev": "cross-env APP_ENV=development python -m alembic upgrade head", - "migrate:prod": "cross-env APP_ENV=production python -m alembic upgrade head", - "migrate:test": "cross-env APP_ENV=testing python -m alembic upgrade head", - "seed:palettes:local": "cross-env APP_ENV=local python scripts/seed_palettes.py", - "seed:palettes:dev": "cross-env APP_ENV=development python scripts/seed_palettes.py", - "seed:palettes:prod": "cross-env APP_ENV=production python scripts/seed_palettes.py", - "seed:palettes:test": "cross-env APP_ENV=testing python scripts/seed_palettes.py", - "seed:superadmin:local": "cross-env APP_ENV=local python scripts/seed_superadmin.py", - "seed:superadmin:dev": "cross-env APP_ENV=development python scripts/seed_superadmin.py", - "seed:superadmin:prod": "cross-env APP_ENV=production python scripts/seed_superadmin.py", - "seed:superadmin:test": "cross-env APP_ENV=testing python scripts/seed_superadmin.py" - }, - "dependencies": {}, - "devDependencies": { - "cross-env": "^7.0.3" - } -} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5fe83e6..687aaa9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,5 @@ bcrypt>=4.0.1 pyjwt>=2.8.0 email-validator>=2.1.0 redis==7.1.0 -requests==2.32.5 \ No newline at end of file +requests==2.32.5 +typer==0.21.1 \ No newline at end of file diff --git a/run.py b/run.py index 3d26edb..6323aaf 100644 --- a/run.py +++ b/run.py @@ -10,6 +10,4 @@ if __name__ == "__main__": host=settings.HOST, port=settings.PORT, reload=settings.APP_ENV == "development", - ) - -# Force Reload + ) \ No newline at end of file