From a2bc84205c3369ea9a85d253bccf931c4d478b48 Mon Sep 17 00:00:00 2001 From: Adarsh2709 Date: Fri, 25 Sep 2026 13:24:50 +0530 Subject: [PATCH 01/11] fix(ci): fix pip cache dependency path, package init, and test execution path --- .github/workflows/ci.yml | 8 +++----- backend/__init__.py | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) create mode 100644 backend/__init__.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b67bb8e..fc850fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..91b66c4 --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1 @@ +"""SmallOps backend package.""" From 351e2181a0b6dab79d74b7343f49fb4b1dd48690 Mon Sep 17 00:00:00 2001 From: Adarsh2709 Date: Fri, 25 Sep 2026 13:37:16 +0530 Subject: [PATCH 02/11] fix(tests): prevent global boto3 module pollution and update test assertions --- backend/api/routes_share.py | 4 ++-- backend/tests/test_auth.py | 3 +-- backend/tests/test_deploy.py | 4 ++-- backend/tests/test_maintenance_orchestrator.py | 13 +++++++------ backend/tests/test_routes_maintenance.py | 11 ++++++----- backend/tests/test_safety_guards.py | 13 +++++++------ backend/tests/test_share.py | 1 + 7 files changed, 26 insertions(+), 23 deletions(-) diff --git a/backend/api/routes_share.py b/backend/api/routes_share.py index c5a7ecb..a3af185 100644 --- a/backend/api/routes_share.py +++ b/backend/api/routes_share.py @@ -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() diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index f1acb57..c2576b1 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -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): diff --git a/backend/tests/test_deploy.py b/backend/tests/test_deploy.py index 1ad78a7..836bfbd 100644 --- a/backend/tests/test_deploy.py +++ b/backend/tests/test_deploy.py @@ -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) diff --git a/backend/tests/test_maintenance_orchestrator.py b/backend/tests/test_maintenance_orchestrator.py index 55a2b18..faaffda 100644 --- a/backend/tests/test_maintenance_orchestrator.py +++ b/backend/tests/test_maintenance_orchestrator.py @@ -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() diff --git a/backend/tests/test_routes_maintenance.py b/backend/tests/test_routes_maintenance.py index fbbd1d5..64740df 100644 --- a/backend/tests/test_routes_maintenance.py +++ b/backend/tests/test_routes_maintenance.py @@ -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() diff --git a/backend/tests/test_safety_guards.py b/backend/tests/test_safety_guards.py index ac874a3..57607e4 100644 --- a/backend/tests/test_safety_guards.py +++ b/backend/tests/test_safety_guards.py @@ -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() diff --git a/backend/tests/test_share.py b/backend/tests/test_share.py index 1907e02..0e4cf80 100644 --- a/backend/tests/test_share.py +++ b/backend/tests/test_share.py @@ -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, From 27aa5535516046fe3f5e9ca8a01a2533c88edcd9 Mon Sep 17 00:00:00 2001 From: Adarsh2709 Date: Fri, 25 Sep 2026 13:45:37 +0530 Subject: [PATCH 03/11] fix(security): enforce require_editor on deploy and revert routes, update e2e test assertion --- backend/api/routes_deploy.py | 5 +++-- backend/api/routes_timeline.py | 2 +- backend/tests/test_e2e_smoke.py | 4 +--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/backend/api/routes_deploy.py b/backend/api/routes_deploy.py index edc8591..27ccf37 100644 --- a/backend/api/routes_deploy.py +++ b/backend/api/routes_deploy.py @@ -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 @@ -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.""" diff --git a/backend/api/routes_timeline.py b/backend/api/routes_timeline.py index 1ef4cf6..a794439 100644 --- a/backend/api/routes_timeline.py +++ b/backend/api/routes_timeline.py @@ -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. diff --git a/backend/tests/test_e2e_smoke.py b/backend/tests/test_e2e_smoke.py index d9bcb43..bb77c20 100644 --- a/backend/tests/test_e2e_smoke.py +++ b/backend/tests/test_e2e_smoke.py @@ -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") From a4def36b5d643fb294b7e69d153cbed96d8bada5 Mon Sep 17 00:00:00 2001 From: Adarsh2709 Date: Mon, 28 Sep 2026 23:07:23 +0530 Subject: [PATCH 04/11] chore(git): ignore build_lambda/ and zip files in .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 4c2ad28..6b31885 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ __pycache__/ *.egg dist/ build/ +build_lambda/ +*.zip .venv/ venv/ .pytest_cache/ From e0584aaf50da94bed44b45b4d47faf4aad03ee91 Mon Sep 17 00:00:00 2001 From: Adarsh2709 Date: Tue, 29 Sep 2026 15:00:50 +0530 Subject: [PATCH 05/11] fix(auth): default authenticated Cognito users to editor role for app deployments --- backend/auth/cognito_client.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/auth/cognito_client.py b/backend/auth/cognito_client.py index 7d41479..d39b097 100644 --- a/backend/auth/cognito_client.py +++ b/backend/auth/cognito_client.py @@ -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", ""), @@ -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", ""), @@ -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", "")), From dbad3086fa3484d6c37171c282472940cb15be07 Mon Sep 17 00:00:00 2001 From: Adarsh2709 Date: Tue, 29 Sep 2026 15:01:42 +0530 Subject: [PATCH 06/11] fix(deploy): enforce single-architecture manifest in backend Docker build for AWS Lambda --- backend/deploy_aws.sh | 2 +- deploy_backend_lambda.ps1 | 181 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 deploy_backend_lambda.ps1 diff --git a/backend/deploy_aws.sh b/backend/deploy_aws.sh index 823b1f3..3fadaad 100755 --- a/backend/deploy_aws.sh +++ b/backend/deploy_aws.sh @@ -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..." diff --git a/deploy_backend_lambda.ps1 b/deploy_backend_lambda.ps1 new file mode 100644 index 0000000..0e8060b --- /dev/null +++ b/deploy_backend_lambda.ps1 @@ -0,0 +1,181 @@ +# deploy_backend_lambda.ps1 +# Deploys SmallOps backend FastAPI application to AWS Lambda via Amazon ECR. + +[CmdletBinding()] +param( + [string]$Region = "ap-south-1", + [string]$FunctionName = "deployed-backend-SmallOpsBackendFunction-8eszVaco3RkA", + [string]$RepoName = "deployedbackendde08ec99/smallopsbackendfunction7579f802repo", + [string]$Tag = "v$(Get-Date -Format 'yyyyMMdd-HHmmss')" +) + +$ErrorActionPreference = "Continue" + +Write-Host "=========================================" -ForegroundColor Cyan +Write-Host " SmallOps Backend AWS Lambda Deployment" -ForegroundColor Cyan +Write-Host "=========================================" -ForegroundColor Cyan + +# 1. Check AWS Credentials +Write-Host "`n[1/6] Checking AWS credentials..." -ForegroundColor Yellow +try { + $callerIdentity = aws sts get-caller-identity --output json | ConvertFrom-Json + $accountId = $callerIdentity.Account + Write-Host " -> Authenticated as AWS Account: $accountId (User: $($callerIdentity.Arn))" -ForegroundColor Green +} catch { + Write-Error "AWS CLI is not authenticated or not found. Please verify your AWS credentials." + exit 1 +} + +# 2. Check Docker Daemon +Write-Host "`n[2/6] Checking Docker status..." -ForegroundColor Yellow +$dockerWorking = $false +try { + $dockerVersion = & docker version --format '{{.Server.Version}}' 2>$null + if ($dockerVersion -and $LASTEXITCODE -eq 0) { + $dockerWorking = $true + Write-Host " -> Docker daemon is running (Server version: $dockerVersion)." -ForegroundColor Green + } +} catch { + # ignore +} + +if (-not $dockerWorking) { + Write-Host " Docker daemon is not running or not responding!" -ForegroundColor Red + Write-Host " Please open Docker Desktop and wait until it finishes starting." -ForegroundColor Yellow + exit 1 +} + +# 3. Log into Amazon ECR +Write-Host "`n[3/6] Logging into Amazon ECR ($Region)..." -ForegroundColor Yellow +$ecrRegistry = "$accountId.dkr.ecr.$Region.amazonaws.com" +$ecrImageUri = "$ecrRegistry/${RepoName}:$Tag" +$ecrLatestUri = "$ecrRegistry/${RepoName}:latest" + +$loginPassword = aws ecr get-login-password --region $Region +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to retrieve ECR login password." + exit 1 +} +$loginPassword | docker login --username AWS --password-stdin $ecrRegistry +if ($LASTEXITCODE -ne 0) { + Write-Error "Docker login to ECR failed." + exit 1 +} +Write-Host " -> Successfully logged into ECR." -ForegroundColor Green + +# 4. Build and Tag Docker Image +Write-Host "`n[4/6] Building Docker image (linux/amd64)..." -ForegroundColor Yellow +Write-Host " Target Image: $ecrImageUri" -ForegroundColor Gray + +# Ensure we run from workspace root +$rootDir = $PSScriptRoot +Set-Location $rootDir + +docker build --provenance=false --platform linux/amd64 -t "smallops-backend:$Tag" -t "smallops-backend:latest" -f backend/Dockerfile backend +if ($LASTEXITCODE -ne 0) { + Write-Error "Docker build failed." + exit 1 +} + +docker tag "smallops-backend:$Tag" $ecrImageUri +docker tag "smallops-backend:latest" $ecrLatestUri + +Write-Host " Pushing image to ECR..." -ForegroundColor Yellow +docker push $ecrImageUri +docker push $ecrLatestUri +if ($LASTEXITCODE -ne 0) { + Write-Error "Docker push to ECR failed." + exit 1 +} +Write-Host " -> Pushed $ecrImageUri successfully." -ForegroundColor Green + +# 5. Update AWS Lambda Function Code +Write-Host "`n[5/6] Updating AWS Lambda Function Code..." -ForegroundColor Yellow +Write-Host " Function: $FunctionName" -ForegroundColor Gray + +aws lambda update-function-code ` + --function-name $FunctionName ` + --image-uri $ecrImageUri ` + --region $Region | Out-Null + +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to update Lambda function code." + exit 1 +} + +Write-Host " Waiting for Lambda code update to complete..." -ForegroundColor Yellow +aws lambda wait function-updated --function-name $FunctionName --region $Region +Write-Host " -> Lambda function code updated." -ForegroundColor Green + +# 6. Update Environment Variables from .env +Write-Host "`n[6/6] Synchronizing Environment Variables from .env..." -ForegroundColor Yellow +$envPath = Join-Path $rootDir ".env" +$envVars = @{} + +if (Test-Path $envPath) { + Get-Content $envPath | ForEach-Object { + $line = $_.Trim() + if ($line -and -not $line.StartsWith("#") -and $line.Contains("=")) { + $parts = $line.Split("=", 2) + $key = $parts[0].Trim() + $val = $parts[1].Trim() + $reservedKeys = @("AWS_REGION", "AWS_DEFAULT_REGION", "AWS_PROFILE", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN") + if ($reservedKeys -notcontains $key) { + $envVars[$key] = $val + } + } + } +} + +# Ensure PORT is 8000 for Lambda Web Adapter +$envVars["PORT"] = "8000" + +$tempEnvFile = Join-Path $rootDir "scratch_lambda_env.json" +try { + $envObj = @{ Variables = $envVars } + $jsonContent = $envObj | ConvertTo-Json -Depth 5 + $utf8NoBom = New-Object System.Text.UTF8Encoding $false + [System.IO.File]::WriteAllText($tempEnvFile, $jsonContent, $utf8NoBom) + + aws lambda update-function-configuration ` + --function-name $FunctionName ` + --environment "file://$tempEnvFile" ` + --region $Region | Out-Null + + if ($LASTEXITCODE -ne 0) { + Write-Warning "Could not update environment variables via CLI." + } else { + Write-Host " Waiting for configuration update to complete..." -ForegroundColor Yellow + aws lambda wait function-updated --function-name $FunctionName --region $Region + Write-Host " -> Environment variables synchronized successfully." -ForegroundColor Green + } +} finally { + if (Test-Path $tempEnvFile) { + Remove-Item -Force $tempEnvFile + } +} + +# 7. Verification & Health Check +Write-Host "`n=========================================" -ForegroundColor Green +Write-Host "DEPLOYMENT FINISHED!" -ForegroundColor Green +Write-Host "=========================================" -ForegroundColor Green +try { + $apiUrl = aws cloudformation describe-stacks ` + --stack-name deployed-backend ` + --region $Region ` + --query "Stacks[0].Outputs[?OutputKey=='BackendApiUrl'].OutputValue" ` + --output text + + if ($apiUrl) { + Write-Host "Backend API URL: $apiUrl" -ForegroundColor Cyan + Write-Host "Testing /health endpoint..." -ForegroundColor Yellow + Start-Sleep -Seconds 3 + $healthUrl = "$($apiUrl.TrimEnd('/'))/health" + $healthResponse = curl.exe -s -m 15 $healthUrl + Write-Host "Response: $healthResponse" -ForegroundColor Green + } +} catch { + Write-Host "Live endpoint verification skipped." -ForegroundColor Gray +} + +Write-Host "`nSmallOps Backend is live!" -ForegroundColor Green From cb20617e91c4ddcb5007556133ea4da204ae6c6c Mon Sep 17 00:00:00 2001 From: Adarsh2709 Date: Tue, 29 Sep 2026 15:01:54 +0530 Subject: [PATCH 07/11] feat(deploy): add automated AWS S3 deployment script for React frontend --- deploy_frontend_s3.ps1 | 82 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 deploy_frontend_s3.ps1 diff --git a/deploy_frontend_s3.ps1 b/deploy_frontend_s3.ps1 new file mode 100644 index 0000000..eb4d7df --- /dev/null +++ b/deploy_frontend_s3.ps1 @@ -0,0 +1,82 @@ +# deploy_frontend_s3.ps1 +# Builds and deploys the SmallOps React frontend to AWS S3 Static Website Hosting. + +[CmdletBinding()] +param( + [string]$Region = "ap-south-1" +) + +$ErrorActionPreference = "Stop" + +Write-Host "=========================================" -ForegroundColor Cyan +Write-Host " SmallOps Frontend S3 Deployment" -ForegroundColor Cyan +Write-Host "=========================================" -ForegroundColor Cyan + +# 1. Check AWS Account +$callerIdentity = aws sts get-caller-identity --output json | ConvertFrom-Json +$accountId = $callerIdentity.Account +$bucketName = "smallops-frontend-$accountId" +Write-Host " -> AWS Account: $accountId" -ForegroundColor Green +Write-Host " -> Target S3 Bucket: $bucketName" -ForegroundColor Green + +# 2. Get Live Backend URL from CloudFormation or .env +$apiUrl = aws cloudformation describe-stacks ` + --stack-name deployed-backend ` + --region $Region ` + --query "Stacks[0].Outputs[?OutputKey=='BackendApiUrl'].OutputValue" ` + --output text + +if (-not $apiUrl -or $apiUrl -eq "None") { + $apiUrl = "https://wawdoi6o2gmixk4uwy3aidzfle0lwnya.lambda-url.ap-south-1.on.aws" +} +$apiUrl = $apiUrl.TrimEnd('/') +Write-Host " -> Backend API URL: $apiUrl" -ForegroundColor Green + +# 3. Synchronize frontend/.env +$rootDir = $PSScriptRoot +$frontendDir = Join-Path $rootDir "frontend" +$frontendEnvPath = Join-Path $frontendDir ".env" +$rootEnvPath = Join-Path $rootDir ".env" + +$userPoolId = "ap-south-1_txppSeJhO" +$clientId = "7eqigcajenecjughu2hg0dgn4q" + +if (Test-Path $rootEnvPath) { + Get-Content $rootEnvPath | ForEach-Object { + $line = $_.Trim() + if ($line.StartsWith("COGNITO_USER_POOL_ID=")) { $userPoolId = $line.Split("=")[1].Trim() } + if ($line.StartsWith("COGNITO_APP_CLIENT_ID=")) { $clientId = $line.Split("=")[1].Trim() } + } +} + +$frontendEnvContent = @" +VITE_API_BASE_URL=$apiUrl +VITE_COGNITO_USER_POOL_ID=$userPoolId +VITE_COGNITO_APP_CLIENT_ID=$clientId +"@ + +Set-Content -Path $frontendEnvPath -Value $frontendEnvContent -Encoding UTF8 +Write-Host " -> Synchronized frontend/.env with live backend URL and Cognito IDs." -ForegroundColor Green + +# 4. Build Frontend Bundle +Write-Host "`nBuilding React frontend..." -ForegroundColor Yellow +Set-Location $frontendDir +npm run build +if ($LASTEXITCODE -ne 0) { + Write-Error "Frontend build failed." + exit 1 +} + +# 5. Upload to S3 +Write-Host "`nSyncing build artifacts to s3://$bucketName/..." -ForegroundColor Yellow +aws s3 sync dist/ "s3://$bucketName/" --delete --region $Region +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to sync to S3." + exit 1 +} + +$websiteUrl = "http://$bucketName.s3-website.$Region.amazonaws.com" +Write-Host "`n=========================================" -ForegroundColor Green +Write-Host "FRONTEND DEPLOYMENT COMPLETE!" -ForegroundColor Green +Write-Host "=========================================" -ForegroundColor Green +Write-Host "Live App URL: $websiteUrl" -ForegroundColor Cyan From 2c44a4ef5fc60e741d604923f62adf424116983f Mon Sep 17 00:00:00 2001 From: Adarsh2709 Date: Tue, 29 Sep 2026 15:02:05 +0530 Subject: [PATCH 08/11] fix(frontend/auth): implement automatic token refresh and session handling via Cognito --- frontend/src/api/auth.ts | 56 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index 882997e..5999eb1 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -101,3 +101,59 @@ export function resendConfirmationCode(email: string): Promise { }); } +export function getValidToken(): Promise { + return new Promise((resolve) => { + if (!poolData.UserPoolId || !poolData.ClientId) { + const fallbackToken = localStorage.getItem('bb_token') || sessionStorage.getItem('bb_token'); + return resolve(fallbackToken); + } + + const cognitoUser = userPool.getCurrentUser(); + if (!cognitoUser) { + const fallbackToken = localStorage.getItem('bb_token') || sessionStorage.getItem('bb_token'); + if (fallbackToken) { + try { + const payload = JSON.parse(atob(fallbackToken.split('.')[1])); + if (payload.exp && payload.exp * 1000 < Date.now()) { + localStorage.removeItem('bb_token'); + sessionStorage.removeItem('bb_token'); + return resolve(null); + } + } catch { + // If token format is not standard JWT, return fallback + } + } + return resolve(fallbackToken); + } + + cognitoUser.getSession((err: any, session: any) => { + if (err || !session || !session.isValid()) { + console.warn("Cognito session invalid or refresh failed:", err); + localStorage.removeItem('bb_token'); + sessionStorage.removeItem('bb_token'); + return resolve(null); + } + + const freshToken = session.getIdToken().getJwtToken(); + if (sessionStorage.getItem('bb_token')) { + sessionStorage.setItem('bb_token', freshToken); + } else { + localStorage.setItem('bb_token', freshToken); + } + return resolve(freshToken); + }); + }); +} + +export function logoutCognito(): void { + const cognitoUser = userPool.getCurrentUser(); + if (cognitoUser) { + cognitoUser.signOut(); + } + localStorage.removeItem('bb_token'); + localStorage.removeItem('bb_user'); + sessionStorage.removeItem('bb_token'); + sessionStorage.removeItem('bb_user'); +} + + From edb4a53417fb7f1c0feaf7e527a8abacf322bdb2 Mon Sep 17 00:00:00 2001 From: Adarsh2709 Date: Tue, 29 Sep 2026 15:02:20 +0530 Subject: [PATCH 09/11] fix(frontend/client): integrate getValidToken in central API client and purge on 401 --- frontend/src/api/client.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 75d1319..be61953 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,23 +1,19 @@ +import { getValidToken } from './auth'; + // ── Central API client ────────────────────────────────────────────────────── // All backend calls go through here so auth headers are always injected. // The Vite proxy forwards /apps /deploy /share /health to http://127.0.0.1:8000 // In production, we use VITE_API_BASE_URL -const BASE = import.meta.env.VITE_API_BASE_URL || ''; +const BASE = (import.meta.env.VITE_API_BASE_URL || '').replace(/\/+$/, ''); const API_URL = BASE; -function getToken(): string | null { - return localStorage.getItem('bb_token') || sessionStorage.getItem('bb_token'); -} - - - async function request( method: string, path: string, body?: unknown, extraHeaders?: Record, ): Promise { - const token = getToken(); + const token = await getValidToken(); const headers: Record = { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), @@ -30,6 +26,11 @@ async function request( body: body !== undefined ? JSON.stringify(body) : undefined, }); + if (res.status === 401) { + localStorage.removeItem('bb_token'); + sessionStorage.removeItem('bb_token'); + } + if (!res.ok) { let msg = `API error ${res.status}`; try { From 55b021c872a604271370ad99d9b4c411e9dced9f Mon Sep 17 00:00:00 2001 From: Adarsh2709 Date: Tue, 29 Sep 2026 15:02:32 +0530 Subject: [PATCH 10/11] fix(frontend/app-view): resolve live app URLs and handle session expiration on app update --- frontend/src/pages/AppView.tsx | 49 +++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/frontend/src/pages/AppView.tsx b/frontend/src/pages/AppView.tsx index 6e0107e..0d46f8e 100644 --- a/frontend/src/pages/AppView.tsx +++ b/frontend/src/pages/AppView.tsx @@ -1,19 +1,26 @@ import { useState, useEffect } from 'react'; -import { useParams, Link } from 'react-router-dom'; +import { useParams, Link, useNavigate } from 'react-router-dom'; import { motion } from 'framer-motion'; import { apiGetApp, apiDeploy } from '../api/client'; import { useDeployStatus } from '../hooks/useDeployStatus'; export const AppView = () => { const { id } = useParams(); + const navigate = useNavigate(); const [appData, setAppData] = useState(null); const [loading, setLoading] = useState(true); const [copied, setCopied] = useState(false); const { status, liveUrl } = useDeployStatus(id); + const apiBase = (import.meta.env.VITE_API_BASE_URL || '').replace(/\/+$/, ''); + const resolvedLiveUrl = liveUrl + ? (liveUrl.startsWith('http') ? liveUrl : `${apiBase}${liveUrl}`) + : null; + // Update state const [updatePrompt, setUpdatePrompt] = useState(''); const [updateStatus, setUpdateStatus] = useState<'idle' | 'pending' | 'failed'>('idle'); + const [errorMessage, setErrorMessage] = useState(null); useEffect(() => { if (id) { @@ -24,18 +31,22 @@ export const AppView = () => { }) .catch(err => { console.error(err); + if (err.status === 401) { + navigate(`/login?return=${encodeURIComponent(window.location.pathname)}`); + return; + } // Fallback demo data setAppData({ title: 'E-Waste Tracker', prompt: 'Build an e-waste drop-off tracker...', status: 'active' }); setLoading(false); }); } - }, [id]); + }, [id, navigate]); const handleCopy = () => { - if (liveUrl) { - navigator.clipboard.writeText(liveUrl); + if (resolvedLiveUrl) { + navigator.clipboard.writeText(resolvedLiveUrl); } else { - navigator.clipboard.writeText(`https://${id}.acfs.live`); + navigator.clipboard.writeText(`https://${id?.slice(0,6) || 'app'}.acfs.live`); } setCopied(true); setTimeout(() => setCopied(false), 2000); @@ -103,14 +114,23 @@ export const AppView = () => { if (!updatePrompt.trim() || status === 'building' || status === 'pending' || updateStatus === 'pending') return; setUpdateStatus('pending'); + setErrorMessage(null); try { const ownerId = localStorage.getItem('bb_user') || sessionStorage.getItem('bb_user') || 'anonymous'; await apiDeploy(id as string, updatePrompt, ownerId, appData?.title || 'Update'); setUpdatePrompt(''); setUpdateStatus('idle'); // Poll will automatically pick up the 'pending'/'building' status - } catch (err) { + } catch (err: any) { console.error("Update failed:", err); + if (err.status === 401) { + setErrorMessage("Your session has expired. Redirecting to login..."); + setTimeout(() => { + navigate(`/login?return=${encodeURIComponent(window.location.pathname)}`); + }, 1200); + return; + } + setErrorMessage(err.message || 'Failed to update application. Try again.'); setUpdateStatus('failed'); } }} @@ -137,7 +157,12 @@ export const AppView = () => { Update App - {updateStatus === 'failed' &&

Failed to update application. Try again.

} + {updateStatus === 'failed' && ( +

{errorMessage || 'Failed to update application. Try again.'}

+ )} + {errorMessage && updateStatus !== 'failed' && ( +

{errorMessage}

+ )}

Describe a change and the agent will update the existing app.

@@ -158,10 +183,10 @@ export const AppView = () => { {/* URL Bar */}
lock - {liveUrl || `https://${id?.slice(0,6) || 'app'}.acfs.live`} - {liveUrl && ( + {resolvedLiveUrl || `https://${id?.slice(0,6) || 'app'}.acfs.live`} + {resolvedLiveUrl && ( { error

Deployment failed

- ) : liveUrl ? ( -