Spaces:
Runtime error
Runtime error
TafadzwaTaps commited on
Commit ·
a41c679
1
Parent(s): 4bc490b
fix: latest updates
Browse files- app.py +3 -6
- config.py +9 -8
- db.py +3 -3
- frontend/api.js +10 -22
- frontend/checkout.html +5 -0
- frontend/index.html +2 -1
- frontend/pro.html +81 -6
- paypal.py +2 -2
app.py
CHANGED
|
@@ -44,8 +44,8 @@ import db as DB
|
|
| 44 |
|
| 45 |
_PLAN_LIMITS = {
|
| 46 |
"free": {
|
| 47 |
-
"daily_scans":
|
| 48 |
-
"daily_repos": 2,
|
| 49 |
"history_limit": 10,
|
| 50 |
"ai_depth": "basic",
|
| 51 |
"repo_scan": True,
|
|
@@ -402,10 +402,7 @@ async def ai_enrich(text: str, findings: list, depth: str = "full") -> dict:
|
|
| 402 |
return {"explanation": "AI unavailable after retries.", "fixes": []}
|
| 403 |
|
| 404 |
# ---- ROUTES ----
|
| 405 |
-
|
| 406 |
-
def dev_mode_status():
|
| 407 |
-
# config.py removed — DEV_MODE no longer applicable
|
| 408 |
-
return {"dev_mode": False, "status": "PRODUCTION MODE"}
|
| 409 |
|
| 410 |
@app.post("/auth/register")
|
| 411 |
def register(req: RegisterRequest, request: Request):
|
|
|
|
| 44 |
|
| 45 |
_PLAN_LIMITS = {
|
| 46 |
"free": {
|
| 47 |
+
"daily_scans": 10, # Improved free tier: 10 file scans per day
|
| 48 |
+
"daily_repos": 2, # 2 repository scans per day
|
| 49 |
"history_limit": 10,
|
| 50 |
"ai_depth": "basic",
|
| 51 |
"repo_scan": True,
|
|
|
|
| 402 |
return {"explanation": "AI unavailable after retries.", "fixes": []}
|
| 403 |
|
| 404 |
# ---- ROUTES ----
|
| 405 |
+
|
|
|
|
|
|
|
|
|
|
| 406 |
|
| 407 |
@app.post("/auth/register")
|
| 408 |
def register(req: RegisterRequest, request: Request):
|
config.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
-
# CONFIG — single source of truth
|
| 5 |
-
# DEV_MODE=true in .env → all plan gates unlocked
|
| 6 |
-
# DEV_MODE=false (or unset) → production gating enforced
|
| 7 |
-
# ============================================================
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
config.py — Application configuration
|
| 3 |
+
========================================
|
| 4 |
+
DEV_MODE has been removed. All plan gates are enforced in production.
|
| 5 |
+
Configuration is read from environment variables only.
|
| 6 |
+
"""
|
| 7 |
|
| 8 |
+
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
# App base URL for redirect links (e.g. PayPal return URLs)
|
| 11 |
+
APP_BASE_URL: str = os.getenv("APP_BASE_URL", "http://localhost:3000")
|
db.py
CHANGED
|
@@ -202,8 +202,8 @@ def check_and_increment_scan(user_id: str) -> tuple[bool, str]:
|
|
| 202 |
update_user(user_id, {"scans_today": 1, "last_scan_date": today})
|
| 203 |
return True, "ok"
|
| 204 |
|
| 205 |
-
if count >=
|
| 206 |
-
return False, "Free plan allows
|
| 207 |
|
| 208 |
update_user(user_id, {"scans_today": count + 1})
|
| 209 |
return True, "ok"
|
|
@@ -465,7 +465,7 @@ def _plan_limits_for(plan: str) -> dict:
|
|
| 465 |
"""Return scan limits for a given effective plan."""
|
| 466 |
LIMITS = {
|
| 467 |
"free": {
|
| 468 |
-
"daily_scans":
|
| 469 |
"daily_repos": 2,
|
| 470 |
"history_limit": 10,
|
| 471 |
"ai_depth": "basic",
|
|
|
|
| 202 |
update_user(user_id, {"scans_today": 1, "last_scan_date": today})
|
| 203 |
return True, "ok"
|
| 204 |
|
| 205 |
+
if count >= 10:
|
| 206 |
+
return False, "Free plan allows 10 scans per day. Upgrade to Pro for unlimited scans."
|
| 207 |
|
| 208 |
update_user(user_id, {"scans_today": count + 1})
|
| 209 |
return True, "ok"
|
|
|
|
| 465 |
"""Return scan limits for a given effective plan."""
|
| 466 |
LIMITS = {
|
| 467 |
"free": {
|
| 468 |
+
"daily_scans": 10, # Improved free tier
|
| 469 |
"daily_repos": 2,
|
| 470 |
"history_limit": 10,
|
| 471 |
"ai_depth": "basic",
|
frontend/api.js
CHANGED
|
@@ -19,21 +19,14 @@ function clearAuth() {
|
|
| 19 |
// ── Plan helpers ───────────────────────────────────────────
|
| 20 |
// Source of truth: localStorage values set by getMe() after every page load.
|
| 21 |
// pro_trial counts as full Pro access.
|
| 22 |
-
let _userPlan =
|
| 23 |
let _userLimits = null;
|
| 24 |
try {
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
repo_scan:true, pdf_download:true, advanced_ai:true, api_access:true,
|
| 28 |
-
cve_enrichment:true, scheduled_scans:true, json_export:true };
|
| 29 |
-
} else {
|
| 30 |
-
const s = localStorage.getItem("user_limits");
|
| 31 |
-
if (s) _userLimits = JSON.parse(s);
|
| 32 |
-
}
|
| 33 |
} catch {}
|
| 34 |
|
| 35 |
function isProUser() {
|
| 36 |
-
if (window.DEV_MODE) return true;
|
| 37 |
if (localStorage.getItem("is_pro") === "true") return true;
|
| 38 |
if (localStorage.getItem("trial_active") === "true") return true;
|
| 39 |
const p = localStorage.getItem("user_plan") || "free";
|
|
@@ -50,14 +43,12 @@ function getTrialDaysLeft() {
|
|
| 50 |
}
|
| 51 |
|
| 52 |
function getUserPlan() {
|
| 53 |
-
if (window.DEV_MODE) return "enterprise";
|
| 54 |
return _userPlan;
|
| 55 |
}
|
| 56 |
|
| 57 |
function getUserLimits() { return _userLimits; }
|
| 58 |
|
| 59 |
function cachePlanData(plan, limits) {
|
| 60 |
-
if (window.DEV_MODE) return;
|
| 61 |
_userPlan = plan;
|
| 62 |
_userLimits = limits;
|
| 63 |
localStorage.setItem("user_plan", plan);
|
|
@@ -65,7 +56,6 @@ function cachePlanData(plan, limits) {
|
|
| 65 |
}
|
| 66 |
|
| 67 |
function canAccessFeature(feature) {
|
| 68 |
-
if (window.DEV_MODE) return true;
|
| 69 |
if (isProUser()) return true;
|
| 70 |
// Free plan has limited access
|
| 71 |
const freeFeatures = { repo_scan: true, basic_scan: true };
|
|
@@ -195,8 +185,8 @@ async function analyzeCode(text) {
|
|
| 195 |
body: JSON.stringify({ text })
|
| 196 |
});
|
| 197 |
const data = await safeJson(res);
|
| 198 |
-
//
|
| 199 |
-
if (
|
| 200 |
cachePlanData(data.plan, null);
|
| 201 |
document.dispatchEvent(new CustomEvent("planUpdated", { detail: data }));
|
| 202 |
}
|
|
@@ -234,13 +224,11 @@ async function getMe() {
|
|
| 234 |
try {
|
| 235 |
const res = await apiRequest("/api/me");
|
| 236 |
const data = await safeJson(res);
|
| 237 |
-
if (
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
cachePlanData(data.plan, data.limits || null);
|
| 243 |
-
}
|
| 244 |
return data;
|
| 245 |
} catch (err) {
|
| 246 |
if (err.message.includes("404")) {
|
|
|
|
| 19 |
// ── Plan helpers ───────────────────────────────────────────
|
| 20 |
// Source of truth: localStorage values set by getMe() after every page load.
|
| 21 |
// pro_trial counts as full Pro access.
|
| 22 |
+
let _userPlan = localStorage.getItem("user_plan") || "free";
|
| 23 |
let _userLimits = null;
|
| 24 |
try {
|
| 25 |
+
const s = localStorage.getItem("user_limits");
|
| 26 |
+
if (s) _userLimits = JSON.parse(s);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
} catch {}
|
| 28 |
|
| 29 |
function isProUser() {
|
|
|
|
| 30 |
if (localStorage.getItem("is_pro") === "true") return true;
|
| 31 |
if (localStorage.getItem("trial_active") === "true") return true;
|
| 32 |
const p = localStorage.getItem("user_plan") || "free";
|
|
|
|
| 43 |
}
|
| 44 |
|
| 45 |
function getUserPlan() {
|
|
|
|
| 46 |
return _userPlan;
|
| 47 |
}
|
| 48 |
|
| 49 |
function getUserLimits() { return _userLimits; }
|
| 50 |
|
| 51 |
function cachePlanData(plan, limits) {
|
|
|
|
| 52 |
_userPlan = plan;
|
| 53 |
_userLimits = limits;
|
| 54 |
localStorage.setItem("user_plan", plan);
|
|
|
|
| 56 |
}
|
| 57 |
|
| 58 |
function canAccessFeature(feature) {
|
|
|
|
| 59 |
if (isProUser()) return true;
|
| 60 |
// Free plan has limited access
|
| 61 |
const freeFeatures = { repo_scan: true, basic_scan: true };
|
|
|
|
| 185 |
body: JSON.stringify({ text })
|
| 186 |
});
|
| 187 |
const data = await safeJson(res);
|
| 188 |
+
// Cache plan data from response for usage counters
|
| 189 |
+
if (data.plan && data.usage_limit) {
|
| 190 |
cachePlanData(data.plan, null);
|
| 191 |
document.dispatchEvent(new CustomEvent("planUpdated", { detail: data }));
|
| 192 |
}
|
|
|
|
| 224 |
try {
|
| 225 |
const res = await apiRequest("/api/me");
|
| 226 |
const data = await safeJson(res);
|
| 227 |
+
if (data?.plan) localStorage.setItem("user_plan", data.plan);
|
| 228 |
+
if (data?.is_pro !== undefined) localStorage.setItem("is_pro", data.is_pro ? "true" : "false");
|
| 229 |
+
if (data?.trial_active !== undefined) localStorage.setItem("trial_active", data.trial_active ? "true" : "false");
|
| 230 |
+
if (data?.days_left !== undefined) localStorage.setItem("trial_days_left", String(data.days_left));
|
| 231 |
+
cachePlanData(data.plan, data.limits || null);
|
|
|
|
|
|
|
| 232 |
return data;
|
| 233 |
} catch (err) {
|
| 234 |
if (err.message.includes("404")) {
|
frontend/checkout.html
CHANGED
|
@@ -275,6 +275,11 @@ function esc(s){ return String(s||'').replace(/&/g,'&').replace(/</g,'<')
|
|
| 275 |
document.getElementById('paySub').innerHTML =
|
| 276 |
'<span style="color:var(--warning);"><i class="bi bi-lock me-1"></i>Pro plan required</span> — upgrade below to access all Pro features.';
|
| 277 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
|
| 279 |
// Pre-fill email
|
| 280 |
var email = localStorage.getItem('user_email') || '';
|
|
|
|
| 275 |
document.getElementById('paySub').innerHTML =
|
| 276 |
'<span style="color:var(--warning);"><i class="bi bi-lock me-1"></i>Pro plan required</span> — upgrade below to access all Pro features.';
|
| 277 |
}
|
| 278 |
+
if (p.get('reason') === 'trial_expired') {
|
| 279 |
+
document.getElementById('payTitle').textContent = 'Your Trial Has Ended';
|
| 280 |
+
document.getElementById('paySub').innerHTML =
|
| 281 |
+
'Your 30-day free trial has expired. Upgrade to Pro at <strong>$1.99/month</strong> to continue unlimited scanning, PDF reports, and all advanced features.';
|
| 282 |
+
}
|
| 283 |
|
| 284 |
// Pre-fill email
|
| 285 |
var email = localStorage.getItem('user_email') || '';
|
frontend/index.html
CHANGED
|
@@ -234,7 +234,8 @@
|
|
| 234 |
<div class="price-amount">$0</div>
|
| 235 |
<div class="price-period">forever free</div>
|
| 236 |
<hr style="border-color:var(--border);margin:20px 0;">
|
| 237 |
-
<div class="price-feature"><i class="bi bi-check-lg"></i>
|
|
|
|
| 238 |
<div class="price-feature"><i class="bi bi-check-lg"></i> AI code analysis</div>
|
| 239 |
<div class="price-feature"><i class="bi bi-check-lg"></i> CVE lookup</div>
|
| 240 |
<div class="price-feature"><i class="bi bi-check-lg"></i> API access</div>
|
|
|
|
| 234 |
<div class="price-amount">$0</div>
|
| 235 |
<div class="price-period">forever free</div>
|
| 236 |
<hr style="border-color:var(--border);margin:20px 0;">
|
| 237 |
+
<div class="price-feature"><i class="bi bi-check-lg"></i> 10 scans / day</div>
|
| 238 |
+
<div class="price-feature"><i class="bi bi-check-lg"></i> 2 repo scans / day</div>
|
| 239 |
<div class="price-feature"><i class="bi bi-check-lg"></i> AI code analysis</div>
|
| 240 |
<div class="price-feature"><i class="bi bi-check-lg"></i> CVE lookup</div>
|
| 241 |
<div class="price-feature"><i class="bi bi-check-lg"></i> API access</div>
|
frontend/pro.html
CHANGED
|
@@ -920,24 +920,99 @@ function setMode(mode) {
|
|
| 920 |
var sideEl = document.getElementById('sidebarEmail');
|
| 921 |
if (sideEl && email) sideEl.textContent = email;
|
| 922 |
|
| 923 |
-
// Load /
|
| 924 |
-
fetch(API + '/
|
| 925 |
-
.then(function(r) {
|
|
|
|
|
|
|
|
|
|
| 926 |
.then(function(raw) {
|
|
|
|
| 927 |
var d = raw.data || raw;
|
|
|
|
| 928 |
if (d.email) {
|
| 929 |
if (sideEl) sideEl.textContent = d.email;
|
| 930 |
localStorage.setItem('user_email', d.email);
|
| 931 |
}
|
| 932 |
-
|
| 933 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 934 |
}
|
|
|
|
|
|
|
|
|
|
| 935 |
})
|
| 936 |
-
.catch(function(){
|
|
|
|
|
|
|
|
|
|
| 937 |
|
| 938 |
setupZipDrop();
|
| 939 |
})();
|
| 940 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 941 |
function doLogout() {
|
| 942 |
localStorage.clear();
|
| 943 |
window.location.href = 'login.html';
|
|
|
|
| 920 |
var sideEl = document.getElementById('sidebarEmail');
|
| 921 |
if (sideEl && email) sideEl.textContent = email;
|
| 922 |
|
| 923 |
+
// Load /api/me to confirm access and get trial/plan info
|
| 924 |
+
fetch(API + '/api/me', { headers: authHeaders() })
|
| 925 |
+
.then(function(r) {
|
| 926 |
+
if (r.status === 401) { window.location.replace('login.html'); return null; }
|
| 927 |
+
return r.json();
|
| 928 |
+
})
|
| 929 |
.then(function(raw) {
|
| 930 |
+
if (!raw) return;
|
| 931 |
var d = raw.data || raw;
|
| 932 |
+
|
| 933 |
if (d.email) {
|
| 934 |
if (sideEl) sideEl.textContent = d.email;
|
| 935 |
localStorage.setItem('user_email', d.email);
|
| 936 |
}
|
| 937 |
+
|
| 938 |
+
var plan = d.plan || 'free';
|
| 939 |
+
var isPro = d.is_pro || false;
|
| 940 |
+
var trialActive = d.trial_active || false;
|
| 941 |
+
var daysLeft = d.days_left || 0;
|
| 942 |
+
var trialEnd = d.trial_end || '';
|
| 943 |
+
|
| 944 |
+
// Cache plan data for api.js helpers
|
| 945 |
+
localStorage.setItem('user_plan', plan);
|
| 946 |
+
localStorage.setItem('is_pro', isPro ? 'true' : 'false');
|
| 947 |
+
localStorage.setItem('trial_active', trialActive ? 'true' : 'false');
|
| 948 |
+
localStorage.setItem('trial_days_left', String(daysLeft));
|
| 949 |
+
|
| 950 |
+
// Access control: pro, pro_trial (active) and enterprise may enter
|
| 951 |
+
var hasAccess = (plan === 'pro' || plan === 'enterprise') ||
|
| 952 |
+
(plan === 'pro_trial' && trialActive);
|
| 953 |
+
|
| 954 |
+
if (!hasAccess) {
|
| 955 |
+
// Trial expired or plain free user — redirect gracefully
|
| 956 |
+
var reason = (plan === 'pro_trial' && !trialActive) ? 'trial_expired' : 'pro_required';
|
| 957 |
+
window.location.replace('checkout.html?reason=' + reason);
|
| 958 |
+
return;
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
// Show trial countdown banner if on active trial
|
| 962 |
+
if (plan === 'pro_trial' && trialActive && daysLeft > 0) {
|
| 963 |
+
showTrialBanner(daysLeft, trialEnd);
|
| 964 |
}
|
| 965 |
+
|
| 966 |
+
// Update the plan badge in the topbar
|
| 967 |
+
updatePlanBadge(plan, trialActive, daysLeft);
|
| 968 |
})
|
| 969 |
+
.catch(function(e) {
|
| 970 |
+
console.warn('Auth check failed', e);
|
| 971 |
+
// Do NOT redirect on network errors — let the user keep working
|
| 972 |
+
});
|
| 973 |
|
| 974 |
setupZipDrop();
|
| 975 |
})();
|
| 976 |
|
| 977 |
+
function showTrialBanner(daysLeft, trialEnd) {
|
| 978 |
+
var urgency = daysLeft <= 3 ? '#f43f5e' : daysLeft <= 7 ? '#fb923c' : '#00ffa3';
|
| 979 |
+
var banner = document.createElement('div');
|
| 980 |
+
banner.id = 'trialBanner';
|
| 981 |
+
banner.style.cssText = [
|
| 982 |
+
'position:fixed;bottom:16px;left:50%;transform:translateX(-50%);',
|
| 983 |
+
'background:rgba(15,23,42,.95);border:1px solid ' + urgency + '44;',
|
| 984 |
+
'border-radius:12px;padding:10px 20px;display:flex;align-items:center;gap:12px;',
|
| 985 |
+
'font-size:12px;color:var(--text-muted);z-index:9000;',
|
| 986 |
+
'box-shadow:0 8px 32px rgba(0,0,0,.5);backdrop-filter:blur(10px);',
|
| 987 |
+
'animation:popIn .3s ease both;'
|
| 988 |
+
].join('');
|
| 989 |
+
banner.innerHTML =
|
| 990 |
+
'<i class="bi bi-clock" style="color:' + urgency + ';font-size:14px;"></i>' +
|
| 991 |
+
'<span>Your <strong style="color:#fff;">Pro Trial</strong> ends in ' +
|
| 992 |
+
'<strong style="color:' + urgency + ';">' + daysLeft + ' day' + (daysLeft === 1 ? '' : 's') + '</strong>' +
|
| 993 |
+
(trialEnd ? ' · ' + trialEnd : '') + '</span>' +
|
| 994 |
+
'<a href="checkout.html" style="background:linear-gradient(135deg,#5b7bfe,#4361ee);color:#fff;' +
|
| 995 |
+
'border-radius:8px;padding:5px 12px;font-size:11px;font-weight:600;text-decoration:none;' +
|
| 996 |
+
'white-space:nowrap;">Upgrade — $1.99/mo</a>' +
|
| 997 |
+
'<button onclick="document.getElementById(\'trialBanner\').remove()" style="' +
|
| 998 |
+
'background:none;border:none;color:var(--text-faint);cursor:pointer;font-size:16px;line-height:1;' +
|
| 999 |
+
'padding:0 0 0 4px;">×</button>';
|
| 1000 |
+
document.body.appendChild(banner);
|
| 1001 |
+
}
|
| 1002 |
+
|
| 1003 |
+
function updatePlanBadge(plan, trialActive, daysLeft) {
|
| 1004 |
+
var badge = document.querySelector('.pro-badge-tag');
|
| 1005 |
+
if (!badge) return;
|
| 1006 |
+
if (plan === 'pro_trial' && trialActive) {
|
| 1007 |
+
badge.textContent = 'Trial · ' + daysLeft + 'd left';
|
| 1008 |
+
badge.style.color = daysLeft <= 3 ? '#f43f5e' : daysLeft <= 7 ? '#fb923c' : '#00ffa3';
|
| 1009 |
+
} else if (plan === 'enterprise') {
|
| 1010 |
+
badge.textContent = 'Enterprise';
|
| 1011 |
+
} else {
|
| 1012 |
+
badge.textContent = 'Pro';
|
| 1013 |
+
}
|
| 1014 |
+
}
|
| 1015 |
+
|
| 1016 |
function doLogout() {
|
| 1017 |
localStorage.clear();
|
| 1018 |
window.location.href = 'login.html';
|
paypal.py
CHANGED
|
@@ -38,9 +38,9 @@ MODE = os.getenv("PAYPAL_MODE", "sandbox").lower() # "sandbox" | "liv
|
|
| 38 |
APP_BASE_URL = os.getenv("APP_BASE_URL", "http://localhost:3000")
|
| 39 |
|
| 40 |
# Pro plan pricing — change here to update everywhere
|
| 41 |
-
PRO_PRICE_USD = "
|
| 42 |
PRO_CURRENCY = "USD"
|
| 43 |
-
PRO_PLAN_NAME = "
|
| 44 |
|
| 45 |
_BASE = {
|
| 46 |
"sandbox": "https://api-m.sandbox.paypal.com",
|
|
|
|
| 38 |
APP_BASE_URL = os.getenv("APP_BASE_URL", "http://localhost:3000")
|
| 39 |
|
| 40 |
# Pro plan pricing — change here to update everywhere
|
| 41 |
+
PRO_PRICE_USD = "1.99"
|
| 42 |
PRO_CURRENCY = "USD"
|
| 43 |
+
PRO_PLAN_NAME = "SafeAIScan Pro — Unlimited Scans + PDF Reports · $1.99/mo"
|
| 44 |
|
| 45 |
_BASE = {
|
| 46 |
"sandbox": "https://api-m.sandbox.paypal.com",
|