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
36 changes: 34 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
# Dependencies
**/node_modules
**/.venv
**/.git
**/__pycache__

# Build outputs
**/.next
**/__pycache__
**/*.pyc
**/*.pyo

# Version control
**/.git
**/.gitignore

# Test artifacts
**/.pytest_cache
**/tests
**/test_*.py
**/*_test.py
**/.coverage

# Dev-only data
**/chroma_db
**/uploads
**/sql_app.db

# Secrets — pass at runtime via env_file or environment
**/.env
**/.env.*

# Misc
**/bloats

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The bloats/ directory is added to .dockerignore, which correctly prevents it from being copied into the Docker images. However, committing temporary scratchpad scripts, draft proposals, and sensitive files (such as security_incident_report.md and emergent_ventures_proposal.md) to the Git repository is a bad practice. It increases repository bloat and risks leaking sensitive information. It is highly recommended to remove the bloats/ directory from version control entirely and add it to .gitignore.

**/.vscode
**/.claude
**/postman_collection.json
**/lint_results*.txt
**/test_output.txt
**/test.txt
95 changes: 95 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# =============================================================================
# FRONTEND — build stages
# =============================================================================

FROM node:20-alpine AS frontend-deps
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci

FROM frontend-deps AS frontend-builder
WORKDIR /app
COPY frontend/ .

ARG NEXT_PUBLIC_ENVIRONMENT
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_BACKEND_URL_PROD
ARG NEXT_PUBLIC_FRONTEND_URL_PROD
ARG NEXT_PUBLIC_BACKEND_URL_STAGING
ARG NEXT_PUBLIC_FRONTEND_URL_STAGING
ARG NEXT_PUBLIC_BACKEND_URL_DEV
ARG NEXT_PUBLIC_FRONTEND_URL_DEV

ENV NEXT_PUBLIC_ENVIRONMENT=$NEXT_PUBLIC_ENVIRONMENT \
NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \
NEXT_PUBLIC_BACKEND_URL_PROD=$NEXT_PUBLIC_BACKEND_URL_PROD \
NEXT_PUBLIC_FRONTEND_URL_PROD=$NEXT_PUBLIC_FRONTEND_URL_PROD \
NEXT_PUBLIC_BACKEND_URL_STAGING=$NEXT_PUBLIC_BACKEND_URL_STAGING \
NEXT_PUBLIC_FRONTEND_URL_STAGING=$NEXT_PUBLIC_FRONTEND_URL_STAGING \
NEXT_PUBLIC_BACKEND_URL_DEV=$NEXT_PUBLIC_BACKEND_URL_DEV \
NEXT_PUBLIC_FRONTEND_URL_DEV=$NEXT_PUBLIC_FRONTEND_URL_DEV

RUN npm run build

FROM node:20-alpine AS frontend
WORKDIR /app

RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs

COPY --from=frontend-builder /app/public ./public
COPY --from=frontend-builder /app/.next/standalone ./
COPY --from=frontend-builder /app/.next/static ./.next/static

RUN chown -R nextjs:nodejs /app
USER nextjs

ENV NODE_ENV=production \
PORT=3000 \
HOSTNAME="0.0.0.0"

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1

CMD ["node", "server.js"]


# =============================================================================
# BACKEND — build stages
# =============================================================================

FROM python:3.10-slim AS backend-builder
WORKDIR /app

RUN pip install --no-cache-dir uv
ENV UV_HTTP_TIMEOUT=300

COPY backend/pyproject.toml backend/uv.lock ./
RUN uv sync --frozen --no-cache

FROM python:3.10-slim AS backend
WORKDIR /app

RUN pip install --no-cache-dir uv

COPY --from=backend-builder /app/.venv /app/.venv
Comment on lines +75 to +77

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Installing uv in the final backend stage is redundant because the virtual environment (/app/.venv) containing all dependencies has already been built in the backend-builder stage and copied over. Since the PATH is updated to prioritize /app/.venv/bin, the application will run successfully without uv in the final image. Removing this step reduces the final image size and speeds up the build process.

COPY --from=backend-builder /app/.venv /app/.venv


ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1

COPY backend/ .

RUN adduser --disabled-password --gecos '' appuser && \
chown -R appuser:appuser /app && \
chmod +x /app/start.sh
USER appuser

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

CMD ["/app/start.sh"]
11 changes: 0 additions & 11 deletions backend/Pipfile

This file was deleted.

Empty file removed backend/__init__.py
Empty file.
10 changes: 3 additions & 7 deletions backend/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
WhatsAppCampaignMessage,
)


# Alembic Config object
config = context.config

Expand All @@ -41,14 +40,11 @@
POSTGRES_PORT = os.getenv("POSTGRES_PORT", "5432")
POSTGRES_DB = os.getenv("POSTGRES_DB")

if all([POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB]):
if os.getenv("DATABASE_URL"):
config.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL"))
elif all([POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB]):
database_url = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}"
config.set_main_option("sqlalchemy.url", database_url.replace("%", "%%"))
else:
# Fallback: if individual vars are missing, try the full DATABASE_URL (e.g., from Heroku, Render, Railway)
fallback_url = os.getenv("DATABASE_URL")
if fallback_url:
config.set_main_option("sqlalchemy.url", fallback_url.replace("%", "%%"))

# Logging setup
if config.config_file_name is not None:
Expand Down
5 changes: 2 additions & 3 deletions backend/app/api/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from app.services.analysis_agent import generate_followup_content
from app.models.widget import GuestMessage
from app.core.response_wrapper import success_response
from app.core.config import settings as app_settings

router = APIRouter()

Expand Down Expand Up @@ -44,7 +45,7 @@ def get_analytics_overview(
)

total_sessions = query.count()

# Better: Count distinct Guest IDs in sessions query
total_guests = query.with_entities(ChatSession.guest_id).distinct().count()

Expand Down Expand Up @@ -179,8 +180,6 @@ class FollowUpRequest(BaseModel):
type: str # "email" or "transcript"
extra_info: Optional[str] = ""

from app.core.config import settings as app_settings

@router.post("/followup", response_model=None)
async def generate_followup(
request: FollowUpRequest,
Expand Down
23 changes: 23 additions & 0 deletions backend/app/api/business.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,29 @@ def _enrich_plan_fields(response: BusinessResponse, business: Business, db: Sess
response.plan_features = features


@router.post("/business/validate-key", response_model=None)
async def validate_api_key(
key_data: dict, # Using dict to avoid importing ValidateKeyRequest for now or just generic
current_user: User = Depends(get_current_user)
):
"""Validate a Google Gemini API Key."""
api_key = key_data.get("api_key")
if not api_key:
raise HTTPException(status_code=400, detail="API Key is required")

try:
from google import genai
client = genai.Client(api_key=api_key)
client.models.generate_content(
model='gemini-2.0-flash',
contents='Test'
)

except Exception as e:
print(f"Key Validation Failed: {e}")
raise HTTPException(status_code=400, detail=f"Invalid API Key: {str(e)}")


@router.post("/business/generate-intents", response_model=None)
async def generate_intents(
current_user: User = Depends(get_current_user),
Expand Down
3 changes: 1 addition & 2 deletions backend/app/api/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from app.services.agent_service import run_conversation
from app.auth.router import get_current_user
from app.core.response_wrapper import success_response
from app.services.analysis_agent import analyze_session, persist_analysis

# Additional Schema for Updating Settings
from pydantic import BaseModel
Expand Down Expand Up @@ -863,8 +864,6 @@ def get_session_details_widget(
]
})

from app.services.analysis_agent import analyze_session, persist_analysis

@router.post("/session/{session_id}/analyze", response_model=None)
async def analyze_chat_session(session_id: str, db: Session = Depends(get_db)):
# 1. Verify session exists
Expand Down
5 changes: 2 additions & 3 deletions backend/app/auth/router.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from fastapi.responses import RedirectResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session

from app.db.session import get_db
from app.models.user import User
Expand All @@ -12,8 +13,6 @@

router = APIRouter(prefix="/auth", tags=["auth"])

from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

security = HTTPBearer()

# --- Dependency: Get Current User ---
Expand Down
28 changes: 20 additions & 8 deletions backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,24 +85,37 @@ class LocalConfig(BaseConfig):
]
CORS_ALLOW_ORIGIN_REGEX: str = ".*"

class StagingConfig(BaseConfig):
FRONTEND_URI: str = "https://taimakoai.onrender.com"
FRONTEND_REDIRECT_URI: str = "https://taimakoai.onrender.com/auth/callback"

CORS_ORIGINS: List[str] = [
"https://taimakoai.onrender.com",
]
Comment on lines +88 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Hardcoding the staging frontend and backend URLs directly in StagingConfig makes the configuration inflexible. If you deploy staging to a different URL or use dynamic pull request preview environments (e.g., on Render), these hardcoded values will break the application. It is better to load them from environment variables with the current staging URLs as fallbacks, similar to how ProductionConfig is implemented.

Suggested change
class StagingConfig(BaseConfig):
FRONTEND_URI: str = "https://taimakoai.onrender.com"
FRONTEND_REDIRECT_URI: str = "https://taimakoai.onrender.com/auth/callback"
CORS_ORIGINS: List[str] = [
"https://taimakoai.onrender.com",
]
class StagingConfig(BaseConfig):
FRONTEND_URI: str = os.getenv("FRONTEND_STAGING_URI", "https://taimakoai.onrender.com")
FRONTEND_REDIRECT_URI: str = f"{os.getenv('FRONTEND_STAGING_URI', 'https://taimakoai.onrender.com')}/auth/callback"
CORS_ORIGINS: List[str] = [
os.getenv("FRONTEND_STAGING_URI", "https://taimakoai.onrender.com"),
]


ALLOWED_HOSTS: List[str] = [
"taimako.onrender.com",
"taimakoai.onrender.com",
]
Comment on lines +96 to +99

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The ALLOWED_HOSTS configuration is used by the backend's trusted host middleware to validate the incoming Host header. It should only contain the domain(s) where the backend application itself is hosted (e.g., taimako.onrender.com). Including the frontend's domain (taimakoai.onrender.com) in the backend's ALLOWED_HOSTS is a security misconfiguration, as the frontend runs on a completely separate server/container and should not be routing host headers to the backend.

    ALLOWED_HOSTS: List[str] = [
        "taimako.onrender.com",
    ]


class ProductionConfig(BaseConfig):
FRONTEND_URI: str = os.getenv("FRONTEND_LIVE_URI", "https://taimako.dubem.xyz")
FRONTEND_REDIRECT_URI: str = f"{os.getenv('FRONTEND_LIVE_URI', 'https://taimako.dubem.xyz')}/auth/callback"

# Paystack
PAYSTACK_SECRET_KEY: str = os.getenv("PAYSTACK_LIVE_SECRET_KEY", "")
PAYSTACK_PUBLIC_KEY: str = os.getenv("PAYSTACK_LIVE_PUBLIC_KEY", "")

CORS_ORIGINS: List[str] = [
"https://taimako.dubem.xyz",
"https://www.taimako.dubem.xyz"
"https://taimako.dubem.xyz",
"https://www.taimako.dubem.xyz",
]
CORS_ALLOW_ORIGIN_REGEX: str = r"https?://.*"

ALLOWED_HOSTS: List[str] = [
"taimako.dubem.xyz",
"taimako.dubem.xyz",
"api.taimako.dubem.xyz",
"*.taimako.dubem.xyz"
"*.taimako.dubem.xyz",
]
# USE_HTTPS_REDIRECT: bool = True # Uncomment if handling SSL termination directly

Expand All @@ -112,8 +125,7 @@ def get_settings():
if env == "production":
return ProductionConfig()
elif env == "staging":
# Treating staging similar to prod for now, or can be its own class
return ProductionConfig()
return StagingConfig()
return LocalConfig()

settings = get_settings()
12 changes: 6 additions & 6 deletions backend/app/db/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
POSTGRES_PORT = os.getenv("POSTGRES_PORT", "5432")
POSTGRES_DB = os.getenv("POSTGRES_DB")

# Build PostgreSQL URL if all required parts are present
if all([POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB]):
# DATABASE_URL takes priority — cloud platforms (Render, Railway, Heroku) provide this directly
if os.getenv("DATABASE_URL"):
SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL")
Comment on lines +14 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Many cloud platforms (such as Render, Heroku, and Railway) provide the database connection string via the DATABASE_URL environment variable using the postgres:// protocol prefix. However, SQLAlchemy 1.4+ and 2.0+ do not support postgres:// directly and will raise a NoSuchModuleError at runtime. It is highly recommended to automatically replace postgres:// with postgresql:// to ensure compatibility and prevent startup crashes on these platforms.

Suggested change
if os.getenv("DATABASE_URL"):
SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL")
database_url = os.getenv("DATABASE_URL")
if database_url:
SQLALCHEMY_DATABASE_URL = database_url.replace("postgres://", "postgresql://", 1)

# Build from individual vars when DATABASE_URL is absent (docker-compose local dev)
elif all([POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB]):
SQLALCHEMY_DATABASE_URL = (
f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}"
)
# Allow direct DATABASE_URL override (common on Heroku, Render, Railway, etc.)
elif os.getenv("DATABASE_URL"):
SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL")
# Final fallback: SQLite for quick local development, cause why not, eh?
# Final fallback: SQLite for quick local development
else:
DATABASE_PATH = os.getenv("DATABASE_PATH", "./sql_app.db")
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DATABASE_PATH}"
Expand Down
Loading
Loading