Skip to content
Closed
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
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ CodeSecAudit AI acts like an AI senior engineer inside GitHub pull requests, det
## Live Links

| Asset | Link |
|---|---|
|---|---|---|
| Kaggle Notebook | https://www.kaggle.com/code/omchoksi04/codereview |
| Hugging Face Dataset | https://huggingface.co/datasets/OMCHOKSI108/CodeSecAudit-RAG |
| RAG Service (live) | https://OMCHOKSI108-codereview-agent.hf.space |
| API (live) | https://codesec-api.onrender.com |
| Dashboard (live) | https://codesec-dashboard.onrender.com |
| Review UI (live) | https://codesec-review-ui.onrender.com |
| GitHub Repo | https://github.com/OMCHOKSI108/codesec-audit-dataset |
| Deploy PR | https://github.com/OMCHOKSI108/codesec-audit-dataset/pull/1 |
| Website (coming soon) | https://codesec-website.onrender.com |

---

Expand Down Expand Up @@ -156,6 +160,16 @@ docker compose up --build

See [docs/docker.md](docs/docker.md) for RAG mode and build options.

### SaaS Website (Flask)

```bash
pip install -e ".[website]"
flask --app website.app run --port 5000
# Open http://localhost:5000
```

See [docs/website.md](docs/website.md) for env vars and deployment.

---

## GitHub Action Usage
Expand Down Expand Up @@ -274,7 +288,7 @@ The user flow for the SaaS version:
6. Limit reached → Resend email with upgrade prompt
7. Contact owner for custom plans

Owner contact: **omchoksi108@gmail.com**
Owner contact: [omchoksi108@gmail.com](mailto:omchoksi108@gmail.com)

Current status:
- [x] Datasets + RAG corpus
Expand Down Expand Up @@ -311,4 +325,4 @@ CodeSecAudit AI is a **defensive security tool**:

For questions, custom plans, or limit increases:

**omchoksi108@gmail.com**
[omchoksi108@gmail.com](mailto:omchoksi108@gmail.com)
17 changes: 17 additions & 0 deletions deploy/render/website.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM python:3.11-slim

WORKDIR /app

COPY pyproject.toml README.md ./
COPY review_engine/ review_engine/
COPY website/ website/
COPY config/ config/

RUN pip install --no-cache-dir -e ".[website]"

COPY deploy/render/website_start.sh /website_start.sh
RUN chmod +x /website_start.sh

EXPOSE 10000

CMD ["/website_start.sh"]
5 changes: 5 additions & 0 deletions deploy/render/website_start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e

echo "Starting CodeSecAudit Website..."
exec gunicorn website.app:app --bind 0.0.0.0:${PORT:-10000} --workers 2 --timeout 60 --access-logfile -
79 changes: 79 additions & 0 deletions docs/website.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# CodeSecAudit AI — SaaS Website

Flask-based marketing site + dashboard with GitHub OAuth login.

---

## Local Run

```bash
pip install -e ".[website]"
flask --app website.app run --port 5000
# or
python -m website.app
```

Open http://localhost:5000

## Environment Variables

| Variable | Required | Description |
|----------|----------|-------------|
| `CODESEC_API_URL` | No | FastAPI backend URL (default: `http://localhost:8000`) |
| `PUBLIC_WEBSITE_URL` | No | Public website URL (default: `http://localhost:5000`) |
| `GITHUB_CLIENT_ID` | For OAuth | GitHub OAuth App client ID |
| `GITHUB_CLIENT_SECRET` | For OAuth | GitHub OAuth App client secret |
| `GITHUB_CALLBACK_URL` | No | OAuth callback (default: `<PUBLIC_WEBSITE_URL>/auth/github/callback`) |
| `SESSION_SECRET` | For production | Flask session secret key |
| `GITHUB_APP_SLUG` | No | GitHub App slug for install button |
| `MONGODB_URI` | No | MongoDB connection string |
| `MONGODB_DB_NAME` | No | MongoDB database name (default: `codesec_audit`) |
| `RESEND_API_KEY` | No | Resend API key for welcome emails |
| `EMAIL_FROM` | No | From address for emails |
| `OWNER_CONTACT_EMAIL` | No | Contact email (default: `omchoksi108@gmail.com`) |
| `FREE_PR_REVIEWS_PER_MONTH` | No | Free plan limit (default: `30`) |

## GitHub OAuth Setup

1. Go to **Settings → Developer settings → OAuth Apps → New OAuth App**
2. Fill:
- Application name: `CodeSecAudit AI (Dev)`
- Homepage URL: `http://localhost:5000` or your Render URL
- Authorization callback URL: `http://localhost:5000/auth/github/callback` or your Render URL
3. Copy `Client ID` and `Client Secret`
4. Set as `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` env vars

## Render Deployment

1. Create a new **Web Service** in Render dashboard
2. Connect your repo
3. Set:
- **Name:** `codesec-website`
- **Region:** same as API
- **Branch:** `main`
- **Runtime:** Docker
- **Dockerfile path:** `deploy/render/website.Dockerfile`
- **Health Check Path:** `/`
4. Add all env vars from the table above
5. Deploy

Or use the `render.yaml` blueprint for automated setup.

## MongoDB Behavior

- If `MONGODB_URI` is set and reachable: user profiles are stored in MongoDB `users` collection
- If MongoDB is unavailable/unconfigured: in-memory dict fallback (lost on restart)
- Website never crashes due to DB issues

## Resend Welcome Email

- If `RESEND_API_KEY` is set: welcome email sent on first sign-in
- Email uses Resend HTTP API directly (no SDK needed)
- If email fails, login still succeeds (logged warning)

## Limitations

- Usage enforcement is **display-only**; actual limit checking is next step
- GitHub App install button appears if `GITHUB_APP_SLUG` is set; no webhook yet
- No account settings persistence (coming in next iteration)
- Review history depends on FastAPI backend availability
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,13 @@ rag = [
"chromadb>=0.5.0",
"sentence-transformers>=2.2.0",
]
website = [
"flask>=3.0.0",
"requests>=2.31.0",
"gunicorn>=22.0.0",
]
dev = [
"codesec-audit-ai[api,ui,rag]",
"codesec-audit-ai[api,ui,rag,website]",
]

[tool.hatch.build]
Expand Down
35 changes: 35 additions & 0 deletions render.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
services:
- type: web
name: codesec-website
runtime: docker
repo: https://github.com/OMCHOKSI108/codesec-audit-dataset
branch: main
dockerfilePath: ./deploy/render/website.Dockerfile
healthCheckPath: /
envVars:
- key: CODESEC_API_URL
value: https://codesec-api.onrender.com
- key: PUBLIC_WEBSITE_URL
value: https://codesec-website.onrender.com
- key: GITHUB_CLIENT_ID
sync: false
- key: GITHUB_CLIENT_SECRET
sync: false
- key: GITHUB_CALLBACK_URL
value: https://codesec-website.onrender.com/auth/github/callback
- key: SESSION_SECRET
generateValue: true
- key: GITHUB_APP_SLUG
sync: false
- key: MONGODB_URI
sync: false
- key: MONGODB_DB_NAME
value: codesec_audit
- key: RESEND_API_KEY
sync: false
- key: EMAIL_FROM
sync: false
- key: OWNER_CONTACT_EMAIL
value: omchoksi108@gmail.com
- key: FREE_PR_REVIEWS_PER_MONTH
value: "30"
Empty file added website/__init__.py
Empty file.
35 changes: 35 additions & 0 deletions website/api_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import requests
from website.config import Config


def fetch_reviews(limit: int = 50, offset: int = 0) -> list[dict]:
try:
url = f"{Config.CODESEC_API_URL}/reviews"
resp = requests.get(url, params={"limit": limit, "offset": offset}, timeout=10)
if resp.status_code == 200:
return resp.json()
except requests.RequestException:
pass
return []


def fetch_stats() -> dict:
try:
url = f"{Config.CODESEC_API_URL}/stats"
resp = requests.get(url, timeout=10)
if resp.status_code == 200:
return resp.json()
except requests.RequestException:
pass
return {}


def fetch_review(review_id: str) -> dict | None:
try:
url = f"{Config.CODESEC_API_URL}/reviews/{review_id}"
resp = requests.get(url, timeout=10)
if resp.status_code == 200:
return resp.json()
except requests.RequestException:
pass
return None
94 changes: 94 additions & 0 deletions website/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import os
import logging
from flask import Flask, render_template, session, redirect, url_for

from website.config import Config
from website.auth import auth_bp
from website.api_client import fetch_reviews, fetch_stats
from website.usage import get_usage

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s")
logger = logging.getLogger(__name__)


def create_app() -> Flask:
app = Flask(__name__)
app.config.from_object(Config)
app.config["SESSION_COOKIE_HTTPONLY"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"

app.register_blueprint(auth_bp)

@app.context_processor
def inject_globals():
slug = Config.GITHUB_APP_SLUG
return {
"github_app_url": f"https://github.com/apps/{slug}/installations/new" if slug else "",
"owner_email": Config.OWNER_CONTACT_EMAIL,
}

@app.route("/")
def index():
return render_template("index.html")

@app.route("/login")
def login_page():
if session.get("user"):
return redirect(url_for("dashboard"))
return render_template("login.html")

@app.route("/dashboard")
def dashboard():
user = session.get("user")
if not user:
return redirect(url_for("login_page"))
usage = get_usage(user)
reviews = []
try:
reviews = fetch_reviews(limit=5)
except Exception:
pass
return render_template("dashboard.html", usage=usage, reviews=reviews)

@app.route("/reviews")
def reviews_page():
error = None
reviews = []
try:
reviews = fetch_reviews(limit=100)
except Exception as e:
error = f"Could not load reviews from API: {e}"
return render_template("reviews.html", reviews=reviews, error=error)

@app.route("/usage")
def usage_page():
user = session.get("user")
usage = get_usage(user)
return render_template("usage.html", usage=usage)

@app.route("/contact")
def contact_page():
return render_template("contact.html")

@app.route("/settings")
def settings_page():
if not session.get("user"):
return redirect(url_for("login_page"))
return render_template("settings.html")

@app.errorhandler(404)
def not_found(e):
return render_template("error.html", error="Page not found"), 404

@app.errorhandler(500)
def server_error(e):
return render_template("error.html", error="Internal server error"), 500

return app


app = create_app()

if __name__ == "__main__":
port = int(os.environ.get("PORT", 5000))
app.run(host="0.0.0.0", port=port, debug=True)
Loading
Loading