Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,17 @@ jobs:
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: "backend/requirements.txt"

- name: Install backend dependencies
run: |
cd backend
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest
pip install -r backend/requirements.txt

- name: Run backend tests
run: |
cd backend
export PYTHONPATH=.
pytest
pytest backend/
env:
AWS_REGION: ap-south-1
AWS_ACCESS_KEY_ID: mock_access_key
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ __pycache__/
*.egg
dist/
build/
build_lambda/
*.zip
.venv/
venv/
.pytest_cache/
Expand Down
333 changes: 333 additions & 0 deletions AWS_PROJECT_SUBMISSION_REPORT.md

Large diffs are not rendered by default.

Binary file added AWS_PROJECT_SUBMISSION_REPORT.pdf
Binary file not shown.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@

Built for **Bharat Builds Tour 2026 (First Commit)** by WeMakeDevs × AWS.

### 🌐 Live Production Deployment
* **Live Web Application (CloudFront CDN):** [https://d38vsojw3wikvs.cloudfront.net](https://d38vsojw3wikvs.cloudfront.net)
* **Sample Generated Micro-App:** [Morning Attendance System](https://wawdoi6o2gmixk4uwy3aidzfle0lwnya.lambda-url.ap-south-1.on.aws/apps/0623f82e-3deb-4fd2-b58a-b609ecd99132/live)
* **Backend API Base:** `https://wawdoi6o2gmixk4uwy3aidzfle0lwnya.lambda-url.ap-south-1.on.aws`


---

## 1. The Problem
Expand Down
1 change: 1 addition & 0 deletions backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""SmallOps backend package."""
5 changes: 3 additions & 2 deletions backend/api/routes_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

import logging

from fastapi import APIRouter, HTTPException, BackgroundTasks
from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends
from pydantic import BaseModel

from backend.agent.planner import plan_and_execute
from backend.agent.trace_logger import log_steps
from backend.auth.roles import require_editor
from backend.config import get_settings
from backend.deploy.lambda_deployer import deploy_to_lambda
from backend.models.app import App, StepType, TimelineStep
Expand Down Expand Up @@ -100,7 +101,7 @@ def _update_app_status(app_id: str, body: DeployRequest, status: str, function_u
)


@router.post("/{app_id}")
@router.post("/{app_id}", dependencies=[Depends(require_editor)])
async def deploy_app(app_id: str, body: DeployRequest, background_tasks: BackgroundTasks):
"""Start the deploy pipeline in the background and return immediately."""

Expand Down
4 changes: 2 additions & 2 deletions backend/api/routes_share.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ def send_invite_email(
return response


@router.post("/apps/{app_id}/invite")
@router.post("/share/{app_id}/invite")
@router.post("/apps/{app_id}/invite", dependencies=[Depends(require_editor)])
@router.post("/share/{app_id}/invite", dependencies=[Depends(require_editor)])
async def invite_user(app_id: str, body: InviteRequest):
"""Invite a collaborator by email with a Viewer or Editor role."""
settings = get_settings()
Expand Down
2 changes: 1 addition & 1 deletion backend/api/routes_timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async def get_timeline_step(app_id: str, step_id: str):
return step.model_dump()


@router.post("/apps/{app_id}/revert/{step_id}")
@router.post("/apps/{app_id}/revert/{step_id}", dependencies=[Depends(require_editor)])
async def revert_to_step(app_id: str, step_id: str):
"""Revert the live app to the code snapshot at a given timeline step.

Expand Down
9 changes: 6 additions & 3 deletions backend/auth/cognito_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ def verify_cognito_token(
groups = claims.get("cognito:groups", [])
if isinstance(groups, str):
groups = [groups]
role = _normalize_role(groups, claims.get("role", Role.VIEWER.value))
fallback_role = claims.get("role", Role.EDITOR.value)
role = _normalize_role(groups, fallback=fallback_role)
return {
"user_id": claims.get("sub", claims.get("username", "anonymous")),
"email": claims.get("email", ""),
Expand All @@ -195,7 +196,8 @@ def verify_cognito_token(
groups = claims.get("cognito:groups", [])
if isinstance(groups, str):
groups = [groups]
role = _normalize_role(groups, claims.get("role", Role.VIEWER.value))
fallback_role = claims.get("role", Role.EDITOR.value)
role = _normalize_role(groups, fallback=fallback_role)
return {
"user_id": claims.get("sub", claims.get("username", "anonymous")),
"email": claims.get("email", ""),
Expand Down Expand Up @@ -240,7 +242,8 @@ def verify_cognito_token(
groups = claims.get("cognito:groups", [])
if isinstance(groups, str):
groups = [groups]
role = _normalize_role(groups, claims.get("role", Role.VIEWER.value))
fallback_role = claims.get("role", Role.EDITOR.value)
role = _normalize_role(groups, fallback=fallback_role)

return {
"user_id": claims.get("sub", claims.get("username", "")),
Expand Down
2 changes: 1 addition & 1 deletion backend/deploy_aws.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ aws ecr describe-repositories --repository-names ${REPO_NAME} --region $REGION |

# 3. Build and Push Docker image
echo "🔨 Building Docker image..."
docker build --platform linux/amd64 -t ${REPO_NAME}:${IMAGE_TAG} .
docker build --provenance=false --platform linux/amd64 -t ${REPO_NAME}:${IMAGE_TAG} .
docker tag ${REPO_NAME}:${IMAGE_TAG} ${ECR_URI}

echo "⬆️ Pushing Docker image to ECR..."
Expand Down
3 changes: 1 addition & 2 deletions backend/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,7 @@ def test_deploy_editor_returns_200(
assert resp.status_code == 200
data = resp.json()
assert data["app_id"] == "test-app-1"
assert data["status"] == "deployed"
assert data["live_url"] == "https://example.lambda-url.ap-south-1.on.aws/"
assert data["status"] in ("building", "deployed")

@patch("backend.auth.roles.verify_cognito_token")
def test_revert_viewer_returns_403(self, mock_verify, client):
Expand Down
4 changes: 2 additions & 2 deletions backend/tests/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ def test_creates_valid_zip(self):
buf = io.BytesIO(zip_bytes)
with zipfile.ZipFile(buf, "r") as zf:
names = zf.namelist()
assert "app_code.py" in names
assert "index.html" in names
assert "lambda_function.py" in names
# Verify the app code content
assert zf.read("app_code.py").decode() == SAMPLE_CODE
assert zf.read("index.html").decode() == SAMPLE_CODE

def test_handler_is_valid_python(self):
zip_bytes = _package_code(SAMPLE_CODE)
Expand Down
4 changes: 1 addition & 3 deletions backend/tests/test_e2e_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,7 @@ def test_full_pipeline_clarify_deploy_timeline_edit_revert_share(
assert deploy_resp.status_code == 200
deploy_data = deploy_resp.json()
assert deploy_data["app_id"] == app_id
assert deploy_data["status"] == "deployed"
assert deploy_data["live_url"] == live_url
assert deploy_data["steps_logged"] >= 3
assert deploy_data["status"] in ("building", "deployed")

# ── Step 3: Inspect Decision Timeline ────────────────────────────
timeline_resp = client.get(f"/apps/{app_id}/timeline")
Expand Down
13 changes: 7 additions & 6 deletions backend/tests/test_maintenance_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
import sys
from unittest.mock import AsyncMock, MagicMock, patch

# Ensure boto3 and submodule mocks exist in test environments
mock_boto3 = MagicMock()
mock_boto3.__path__ = []
sys.modules["boto3"] = mock_boto3
sys.modules["boto3.dynamodb"] = MagicMock()
sys.modules["boto3.dynamodb.conditions"] = MagicMock()
# Ensure boto3 and submodule mocks exist in test environments if missing
if "boto3" not in sys.modules:
mock_boto3 = MagicMock()
mock_boto3.__path__ = []
sys.modules["boto3"] = mock_boto3
sys.modules["boto3.dynamodb"] = MagicMock()
sys.modules["boto3.dynamodb.conditions"] = MagicMock()

if "botocore" not in sys.modules:
sys.modules["botocore"] = MagicMock()
Expand Down
11 changes: 6 additions & 5 deletions backend/tests/test_routes_maintenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
from unittest.mock import AsyncMock, MagicMock, patch

# Ensure mocks for boto3, jose, and structlog if missing in test environment
mock_boto3 = MagicMock()
mock_boto3.__path__ = []
sys.modules["boto3"] = mock_boto3
sys.modules["boto3.dynamodb"] = MagicMock()
sys.modules["boto3.dynamodb.conditions"] = MagicMock()
if "boto3" not in sys.modules:
mock_boto3 = MagicMock()
mock_boto3.__path__ = []
sys.modules["boto3"] = mock_boto3
sys.modules["boto3.dynamodb"] = MagicMock()
sys.modules["boto3.dynamodb.conditions"] = MagicMock()

if "botocore" not in sys.modules:
sys.modules["botocore"] = MagicMock()
Expand Down
13 changes: 7 additions & 6 deletions backend/tests/test_safety_guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
import sys
from unittest.mock import AsyncMock, MagicMock, patch

# Ensure boto3 mocks for test environments
mock_boto3 = MagicMock()
mock_boto3.__path__ = []
sys.modules["boto3"] = mock_boto3
sys.modules["boto3.dynamodb"] = MagicMock()
sys.modules["boto3.dynamodb.conditions"] = MagicMock()
# Ensure boto3 mocks for test environments if missing
if "boto3" not in sys.modules:
mock_boto3 = MagicMock()
mock_boto3.__path__ = []
sys.modules["boto3"] = mock_boto3
sys.modules["boto3.dynamodb"] = MagicMock()
sys.modules["boto3.dynamodb.conditions"] = MagicMock()

if "botocore" not in sys.modules:
sys.modules["botocore"] = MagicMock()
Expand Down
1 change: 1 addition & 0 deletions backend/tests/test_share.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def test_send_email_params_with_moto(self):
with mock_aws():
ses = boto3.client("ses", region_name=TEST_REGION)
ses.verify_email_identity(EmailAddress=SENDER_EMAIL)
ses.verify_email_identity(EmailAddress=RECIPIENT_EMAIL)

resp = send_invite_email(
to_email=RECIPIENT_EMAIL,
Expand Down
Loading
Loading