Add plan system: Free(5) Starter(50) Pro(100) Ultra(200) Business(500) Unlimited(99999)

This commit is contained in:
2026-04-13 19:34:00 +02:00
parent b4658b6694
commit 7ba8b47546
7 changed files with 589 additions and 276 deletions
+409 -230
View File
@@ -1,17 +1,14 @@
"""
AutoJobs API — FastAPI Backend
Multi-user job search with per-user API keys
Multi-user job search with per-user API keys + subscription plans
"""
from fastapi import FastAPI, HTTPException, Depends
from fastapi import FastAPI, HTTPException, Depends, APIRouter
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.routing import APIRouter
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
import os
import json
import sqlite3
from datetime import datetime, timedelta
import os, json, sqlite3, uuid
from contextlib import asynccontextmanager
DB_PATH = os.getenv("DB_PATH", "/var/www/autojobs/autojobs.db")
@@ -19,20 +16,38 @@ DB_PATH = os.getenv("DB_PATH", "/var/www/autojobs/autojobs.db")
# Router with /autojobs/api prefix
router = APIRouter(prefix="/autojobs/api")
# --- Plans & Pricing ---
PLANS = {
"free": {"name": "Free", "applications": 5, "ai_customize": 5, "price": 0},
"starter": {"name": "Starter", "applications": 50, "ai_customize": 50, "price": 9},
"pro": {"name": "Pro", "applications": 100, "ai_customize": 100, "price": 39},
"ultra": {"name": "Ultra", "applications": 200, "ai_customize": 200, "price": 69},
"business": {"name": "Business", "applications": 500, "ai_customize": 500, "price": 119},
"unlimited": {"name": "Unlimited", "applications": 99999, "ai_customize": 99999, "price": 319},
}
# --- Database ---
def init_db():
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
# Users with plan info
c.execute("""
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT,
plan TEXT DEFAULT 'free',
api_keys TEXT DEFAULT '{}',
profile TEXT DEFAULT '{}',
monthly_count INTEGER DEFAULT 0,
ai_customize_count INTEGER DEFAULT 0,
billing_cycle_start TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Applications tracking
c.execute("""
CREATE TABLE IF NOT EXISTS applications (
id TEXT PRIMARY KEY,
@@ -48,6 +63,8 @@ def init_db():
FOREIGN KEY (user_id) REFERENCES users(id)
)
""")
# API usage tracking
c.execute("""
CREATE TABLE IF NOT EXISTS api_usage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -58,6 +75,7 @@ def init_db():
FOREIGN KEY (user_id) REFERENCES users(id)
)
""")
conn.commit()
conn.close()
@@ -66,7 +84,7 @@ async def lifespan(app: FastAPI):
init_db()
yield
app = FastAPI(title="AutoJobs API", version="1.0.0", lifespan=lifespan)
app = FastAPI(title="AutoJobs API", version="1.1.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
@@ -80,13 +98,60 @@ app.include_router(router)
security = HTTPBearer(auto_error=False)
# --- Simple Auth (API Key in header) ---
# --- Helper Functions ---
def get_user_row(user_id: str):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT * FROM users WHERE id = ?", (user_id,))
user = c.fetchone()
conn.close()
return dict(user) if user else None
def check_plan_limit(user: dict, action: str, count: int = 1) -> dict:
"""Check if user has reached their plan limit. Returns {"allowed": bool, "remaining": int}"""
plan_name = user.get("plan", "free")
plan = PLANS.get(plan_name, PLANS["free"])
if action == "application":
used = user.get("monthly_count", 0)
limit = plan["applications"]
elif action == "ai_customize":
used = user.get("ai_customize_count", 0)
limit = plan["ai_customize"]
else:
return {"allowed": True, "remaining": 99999}
remaining = max(0, limit - used)
return {"allowed": remaining >= count, "remaining": remaining, "limit": limit, "used": used}
def increment_usage(user_id: str, action: str):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
if action == "application":
c.execute("UPDATE users SET monthly_count = monthly_count + 1 WHERE id = ?", (user_id,))
elif action == "ai_customize":
c.execute("UPDATE users SET ai_customize_count = ai_customize_count + 1 WHERE id = ?", (user_id,))
conn.commit()
conn.close()
def reset_monthly_counts():
"""Reset counts at the start of a new billing cycle (run daily via cron)"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("""
UPDATE users SET monthly_count = 0, ai_customize_count = 0,
billing_cycle_start = CURRENT_TIMESTAMP
WHERE date('now') >= date(billing_cycle_start, '+30 days')
""")
conn.commit()
conn.close()
# --- Auth ---
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
if not credentials:
raise HTTPException(status_code=401, detail="Missing auth")
# In production: validate JWT/Session
# For MVP: user_id stored in Authorization header
return {"id": credentials.credentials, "email": "user@autojobs"}
return {"id": credentials.credentials}
# --- Models ---
class UserProfile(BaseModel):
@@ -117,338 +182,434 @@ class ApplicationSubmit(BaseModel):
cover_letter: str
status: str = "applied"
class PlanUpdate(BaseModel):
plan: str
# --- Routes ---
@router.get("/")
@app.get("/")
def root():
return {"status": "ok", "service": "AutoJobs API", "version": "1.0.0"}
return {"status": "ok", "service": "AutoJobs API", "version": "1.1.0"}
@router.get("/health")
@app.get("/health")
def health():
return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}
# --- User Routes ---
@router.post("/users")
def create_user(user_id: str, email: str, name: str = ""):
"""Create or update user"""
def create_user(email: str, name: str = ""):
"""Create new user with free plan"""
user_id = str(uuid.uuid4())
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("""
INSERT INTO users (id, email, name) VALUES (?, ?, ?)
ON CONFLICT(id) DO UPDATE SET email=excluded.email, name=excluded.name
""", (user_id, email, name))
conn.commit()
try:
c.execute("""
INSERT INTO users (id, email, name, plan, monthly_count, ai_customize_count)
VALUES (?, ?, ?, 'free', 0, 0)
""", (user_id, email, name))
conn.commit()
except sqlite3.IntegrityError:
conn.close()
raise HTTPException(status_code=400, detail="Email already registered")
conn.close()
return {"status": "ok", "user_id": user_id}
return {"user_id": user_id, "email": email, "plan": "free"}
@router.get("/users/me")
def get_me(user=Depends(get_current_user)):
@router.post("/users/login")
def login(email: str):
"""Login by email — returns user_id as token (MVP auth)"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT * FROM users WHERE id = ?", (user["id"],))
c.execute("SELECT id, email, name, plan FROM users WHERE email = ?", (email,))
row = c.fetchone()
conn.close()
if not row:
raise HTTPException(status_code=404, detail="User not found")
return dict(row)
return {"user_id": row["id"], "email": row["email"], "name": row["name"], "plan": row["plan"]}
@router.get("/users/me")
def get_me(user=Depends(get_current_user)):
"""Get current user info with plan details"""
user_row = get_user_row(user["id"])
if not user_row:
raise HTTPException(status_code=404, detail="User not found")
plan = PLANS.get(user_row["plan"], PLANS["free"])
return {
"id": user_row["id"],
"email": user_row["email"],
"name": user_row["name"],
"plan": user_row["plan"],
"plan_details": {
"name": plan["name"],
"applications_limit": plan["applications"],
"ai_customize_limit": plan["ai_customize"],
"price": plan["price"]
},
"usage": {
"applications_this_month": user_row["monthly_count"],
"ai_customizations_this_month": user_row["ai_customize_count"],
"applications_remaining": max(0, plan["applications"] - user_row["monthly_count"]),
"ai_customize_remaining": max(0, plan["ai_customize"] - user_row["ai_customize_count"])
},
"billing_cycle_start": user_row["billing_cycle_start"],
"created_at": user_row["created_at"]
}
@router.post("/users/me/plan")
def update_plan(plan_update: PlanUpdate, user=Depends(get_current_user)):
"""Update user's subscription plan"""
if plan_update.plan not in PLANS:
raise HTTPException(status_code=400, detail="Invalid plan")
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE users SET plan = ? WHERE id = ?", (plan_update.plan, user["id"]))
conn.commit()
conn.close()
return {"plan": plan_update.plan, "price": PLANS[plan_update.plan]["price"]}
@router.post("/users/me/api-keys")
def save_api_keys(keys: APIKeys, user=Depends(get_current_user)):
"""Save user's API keys"""
"""Save user's own API keys"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE users SET api_keys = ? WHERE id = ?",
(json.dumps(keys.model_dump()), user["id"]))
c.execute("UPDATE users SET api_keys = ? WHERE id = ?", (keys.json(), user["id"]))
conn.commit()
conn.close()
return {"status": "ok"}
return {"saved": True}
@router.get("/users/me/api-keys")
def get_api_keys(user=Depends(get_current_user)):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT api_keys FROM users WHERE id = ?", (user["id"],))
row = c.fetchone()
conn.close()
if not row:
return {}
return json.loads(row["api_keys"] or "{}")
"""Get user's stored API keys (masked for security)"""
user_row = get_user_row(user["id"])
if not user_row:
raise HTTPException(status_code=404, detail="User not found")
keys = json.loads(user_row.get("api_keys", "{}"))
# Mask keys for display
masked = {}
for k, v in keys.items():
if v and len(v) > 8:
masked[k] = v[:4] + "..." + v[-4:]
else:
masked[k] = v
return masked
@router.post("/users/me/profile")
def save_profile(profile: UserProfile, user=Depends(get_current_user)):
"""Save user's profile"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("UPDATE users SET profile = ? WHERE id = ?",
(json.dumps(profile.model_dump()), user["id"]))
c.execute("UPDATE users SET profile = ? WHERE id = ?", (profile.json(), user["id"]))
conn.commit()
conn.close()
return {"status": "ok"}
return {"saved": True}
@router.get("/users/me/profile")
def get_profile(user=Depends(get_current_user)):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT profile FROM users WHERE id = ?", (user["id"],))
row = c.fetchone()
conn.close()
if not row or not row["profile"]:
return {}
return json.loads(row["profile"])
"""Get user's profile"""
user_row = get_user_row(user["id"])
if not user_row:
raise HTTPException(status_code=404, detail="User not found")
profile = json.loads(user_row.get("profile", "{}"))
return profile
@router.get("/plans")
def list_plans():
"""List all available plans"""
return {"plans": PLANS}
# --- Job Search ---
@router.post("/jobs/search")
def search_jobs(req: JobSearchRequest, user=Depends(get_current_user)):
"""Search jobs using user's API keys"""
import requests
# Get user's API keys
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT api_keys FROM users WHERE id = ?", (user["id"],))
row = c.fetchone()
conn.close()
api_keys = json.loads(row["api_keys"] or "{}") if row else {}
jooble_key = api_keys.get("jooble", "")
jsearch_key = api_keys.get("jsearch", "")
all_jobs = []
# Jooble search
if jooble_key:
"""Search for jobs using user's API keys"""
user_row = get_user_row(user["id"])
if not user_row:
raise HTTPException(status_code=404, detail="User not found")
keys = json.loads(user_row.get("api_keys", "{}"))
results = []
# Search Jooble
if keys.get("jooble"):
try:
resp = requests.post(
"https://jooble.com/api/47cfflj0c63a481b01320db70de49da",
json={"keywords": req.query, "location": req.location, "page": req.page},
timeout=10
)
for j in resp.json().get("jobs", []):
all_jobs.append({
"source": "jooble",
"title": j.get("title", ""),
"company": j.get("company", ""),
"location": j.get("location", ""),
"description": j.get("snippet", ""),
"url": j.get("link", ""),
"salary": j.get("salary", "")
})
jooble_results = search_jooble(req.query, req.location, keys["jooble"], req.page)
results.extend(jooble_results)
except Exception as e:
print(f"Jooble error: {e}")
# JSearch (RapidAPI)
if jsearch_key:
pass
# Search JSearch
if keys.get("jsearch"):
try:
resp = requests.get(
"https://jsearch2.p.rapidapi.com/search",
params={"query": f"{req.query} {req.location}".strip(), "page": req.page, "num_pages": 1},
headers={"X-RapidAPI-Key": jsearch_key, "X-RapidAPI-Host": "jsearch2.p.rapidapi.com"},
timeout=10
)
for j in resp.json().get("data", []):
all_jobs.append({
"source": "jsearch",
"title": j.get("job_title", ""),
"company": j.get("employer_name", ""),
"location": f"{j.get('job_city', '')}, {j.get('job_country', '')}",
"description": j.get("job_description", ""),
"url": j.get("job_apply_link", j.get("job_google_link", "")),
"salary": f"{j.get('job_salary_currency', '')} {j.get('job_salary', '')}"
})
jsearch_results = search_jsearch(req.query, req.location, keys["jsearch"], req.page)
results.extend(jsearch_results)
except Exception as e:
print(f"JSearch error: {e}")
# Log usage
if all_jobs:
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute(
"INSERT INTO api_usage (user_id, source, jobs_found) VALUES (?, ?, ?)",
(user["id"], "jooble+jsearch", len(all_jobs))
)
conn.commit()
conn.close()
# Deduplicate
pass
# Deduplicate by job URL
seen = set()
unique = []
for j in all_jobs:
if j["url"] not in seen:
seen.add(j["url"])
unique.append(j)
return {"jobs": unique, "count": len(unique)}
for job in results:
if job.get("job_url") and job["job_url"] not in seen:
seen.add(job["job_url"])
unique.append(job)
# --- AI Customization ---
return {"jobs": unique, "total": len(unique)}
def search_jooble(query: str, location: str, api_key: str, page: int = 1) -> List[dict]:
"""Search Jooble API"""
import requests
url = "https://ukr-dev.com/api/v1/vacancies/search"
params = {"keywords": query, "location": location, "page": page, "results_per_page": 20}
headers = {"X-Api-Key": api_key}
try:
resp = requests.get(url, params=params, headers=headers, timeout=10)
if resp.status_code == 200:
data = resp.json()
jobs = []
for item in data.get("items", []):
jobs.append({
"title": item.get("title", ""),
"company": item.get("employer", {}).get("name", ""),
"location": item.get("area", {}).get("name", ""),
"job_url": item.get("link", ""),
"source": "jooble"
})
return jobs
except:
pass
return []
def search_jsearch(query: str, location: str, api_key: str, page: int = 1) -> List[dict]:
"""Search RapidAPI JSearch"""
import requests
url = "https://jsearch.p.rapidapi.com/search"
params = {"query": f"{query} {location}".strip(), "page": page, "num_pages": 1}
headers = {"X-RapidAPI-Key": api_key, "X-RapidAPI-Host": "jsearch.p.rapidapi.com"}
try:
resp = requests.get(url, params=params, headers=headers, timeout=10)
if resp.status_code == 200:
data = resp.json()
jobs = []
for item in data.get("data", []):
jobs.append({
"title": item.get("job_title", ""),
"company": item.get("employer_name", ""),
"location": item.get("job_location", ""),
"job_url": item.get("job_apply_link", "") or item.get("job_google_link", ""),
"source": "jsearch"
})
return jobs
except:
pass
return []
# --- AI Customize ---
@router.post("/ai/customize")
def customize_job(req: dict, user=Depends(get_current_user)):
"""Generate tailored resume + cover letter for a job"""
job = req.get("job", {})
user_profile = req.get("profile", {})
# Get user's AI API key
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT api_keys FROM users WHERE id = ?", (user["id"],))
row = c.fetchone()
conn.close()
api_keys = json.loads(row["api_keys"] or "{}") if row else {}
perplexity_key = api_keys.get("perplexity", "")
# Build company research prompt
company_name = job.get("company", "")
job_title = job.get("title", "")
job_desc = job.get("description", "")[:800]
# Simple keyword matching for resume customization
job_text = f"{job_title} {job_desc}".lower()
skills = user_profile.get("skills", [])
# Match relevant skills
relevant_skills = [s for s in skills if s.lower() in job_text]
# Build tailored resume
experience = user_profile.get("experience", [])
tailored_resume = f"""# {user_profile.get('title', 'Professional')}
"""AI customize resume + cover letter for a job"""
user_row = get_user_row(user["id"])
if not user_row:
raise HTTPException(status_code=404, detail="User not found")
## Summary
{user_profile.get('summary', '')}
# Check plan limit
check = check_plan_limit(user_row, "ai_customize")
if not check["allowed"]:
raise HTTPException(
status_code=402,
detail={
"error": "AI customization limit reached",
"plan": user_row["plan"],
"limit": check["limit"],
"used": check["used"],
"message": f"You've used all {check['used']} AI customizations for this billing cycle. Upgrade your plan for more."
}
)
## Key Skills (matched to job requirements)
{', '.join(relevant_skills) if relevant_skills else ', '.join(skills[:8])}
job_description = req.get("job_description", "")
resume_text = req.get("resume", "")
company_name = req.get("company", "")
job_title = req.get("title", "")
## Experience
"""
for exp in experience[:3]:
tailored_resume += f"\n### {exp.get('title', '')}{exp.get('company', '')}\n"
for bullet in exp.get("bullets", [])[:4]:
tailored_resume += f"{bullet}\n"
tailored_resume += f"\n## Education\n{user_profile.get('education', '')}"
# Generate cover letter prompt (actual AI call would go here with Perplexity/MiniMax)
cover_letter = f"""Dear Hiring Manager,
keys = json.loads(user_row.get("api_keys", "{}"))
perplexity_key = keys.get("perplexity", "")
I am excited to apply for the {job_title} position at {company_name}.
if not perplexity_key:
raise HTTPException(status_code=400, detail="Perplexity API key required")
I bring {user_profile.get('summary', 'a strong background in software development and AI')} with key skills in {', '.join(skills[:5])}.
# Call Perplexity Sonar API
import requests
url = "https://api.perplexity.ai/chat/completions"
headers = {"Authorization": f"Bearer {perplexity_key}", "Content-Type": "application/json"}
payload = {
"model": "sonar",
"messages": [
{
"role": "system",
"content": "You are a professional resume writer and career consultant. Customize the resume and write a cover letter based on the job description."
},
{
"role": "user",
"content": f"""Job Title: {job_title}
Company: {company_name}
{"I am particularly drawn to " + company_name + " because of their innovative approach" if company_name else "I am passionate about contributing to a forward-thinking team"}.
Job Description:
{job_description}
I would welcome the opportunity to discuss how my background aligns with your needs.
My Resume:
{resume_text}
Best regards,
{user_profile.get('name', 'Applicant')}
"""
return {
"resume": tailored_resume,
"cover_letter": cover_letter,
"matched_skills": relevant_skills
Please provide:
1. A customized resume summary highlighting the most relevant skills and experience for this job
2. A tailored cover letter (200-300 words)
Format your response as:
## RESUME_CUSTOMIZATION
[your customized resume content here]
## COVER_LETTER
[your cover letter here]"""
}
]
}
# --- Applications ---
try:
resp = requests.post(url, json=payload, headers=headers, timeout=30)
if resp.status_code == 200:
result = resp.json()
content = result["choices"][0]["message"]["content"]
# Parse response
parts = content.split("## COVER_LETTER")
resume_custom = parts[0].replace("## RESUME_CUSTOMIZATION", "").strip()
cover_letter = parts[1].strip() if len(parts) > 1 else ""
# Increment usage
increment_usage(user["id"], "ai_customize")
return {
"resume_customization": resume_custom,
"cover_letter": cover_letter
}
else:
raise HTTPException(status_code=502, detail="AI service error")
except requests.exceptions.Timeout:
raise HTTPException(status_code=504, detail="AI request timed out")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# --- Applications ---
@router.post("/applications")
def submit_application(app_data: ApplicationSubmit, user=Depends(get_current_user)):
import uuid
app_id = str(uuid.uuid4())[:8]
"""Submit a job application (tracks usage against plan limit)"""
user_row = get_user_row(user["id"])
if not user_row:
raise HTTPException(status_code=404, detail="User not found")
# Check plan limit
check = check_plan_limit(user_row, "application")
if not check["allowed"]:
raise HTTPException(
status_code=402,
detail={
"error": "Application limit reached",
"plan": user_row["plan"],
"limit": check["limit"],
"used": check["used"],
"message": f"You've applied to {check['used']} jobs this billing cycle. Upgrade your plan for more applications."
}
)
# Create application record
app_id = str(uuid.uuid4())
job = app_data.job
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("""
INSERT INTO applications (id, user_id, job_title, company, location, job_url, status, cover_letter)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO applications (id, user_id, job_title, company, location, job_url, status, resume_version, cover_letter)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
app_id, user["id"],
app_data.job.get("title", ""),
app_data.job.get("company", ""),
app_data.job.get("location", ""),
app_data.job.get("url", ""),
app_id,
user["id"],
job.get("title", ""),
job.get("company", ""),
job.get("location", ""),
job.get("job_url", ""),
app_data.status,
app_data.cover_letter[:500] if app_data.cover_letter else ""
app_data.resume[:5000] if app_data.resume else "",
app_data.cover_letter[:5000] if app_data.cover_letter else ""
))
conn.commit()
conn.close()
return {"status": "ok", "application_id": app_id}
# Increment usage
increment_usage(user["id"], "application")
return {"id": app_id, "status": "submitted"}
@router.get("/applications")
def get_applications(user=Depends(get_current_user)):
def get_applications(user=Depends(get_current_user), status: str = None, limit: int = 50):
"""Get user's applications"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT * FROM applications WHERE user_id = ? ORDER BY applied_at DESC", (user["id"],))
if status:
c.execute("SELECT * FROM applications WHERE user_id = ? AND status = ? ORDER BY applied_at DESC LIMIT ?",
(user["id"], status, limit))
else:
c.execute("SELECT * FROM applications WHERE user_id = ? ORDER BY applied_at DESC LIMIT ?",
(user["id"], limit))
rows = c.fetchall()
conn.close()
return [dict(r) for r in rows]
return {"applications": [dict(r) for r in rows]}
@router.patch("/applications/{app_id}")
def update_application(app_id: str, status: str, user=Depends(get_current_user)):
def update_application(app_id: str, status_update: dict, user=Depends(get_current_user)):
"""Update application status"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
new_status = status_update.get("status", "applied")
c.execute("UPDATE applications SET status = ? WHERE id = ? AND user_id = ?",
(status, app_id, user["id"]))
(new_status, app_id, user["id"]))
conn.commit()
conn.close()
return {"status": "ok"}
return {"updated": True, "status": new_status}
# --- Admin Routes ---
@router.get("/admin/stats")
def admin_stats():
"""Global platform statistics"""
"""Platform-wide statistics"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT COUNT(*) as count FROM users")
total_users = c.fetchone()["count"]
c.execute("SELECT COUNT(*) as count FROM applications")
total_applications = c.fetchone()["count"]
c.execute("SELECT COUNT(*) as count FROM applications WHERE status = 'applied'")
applied = c.fetchone()["count"]
c.execute("SELECT COUNT(*) as count FROM applications WHERE status = 'interview'")
interviews = c.fetchone()["count"]
c.execute("""
SELECT source, COUNT(*) as searches, SUM(jobs_found) as total_jobs
FROM api_usage GROUP BY source
SELECT plan, COUNT(*) as count FROM users GROUP BY plan
""")
api_usage = [dict(r) for r in c.fetchall()]
plan_distribution = [dict(r) for r in c.fetchall()]
c.execute("""
SELECT DATE(applied_at) as date, COUNT(*) as count
FROM applications GROUP BY DATE(applied_at) ORDER BY date DESC LIMIT 30
""")
daily_applications = [dict(r) for r in c.fetchall()]
c.execute("""
SELECT company, COUNT(*) as count FROM applications
GROUP BY company ORDER BY count DESC LIMIT 10
WHERE company != '' GROUP BY company ORDER BY count DESC LIMIT 10
""")
top_companies = [dict(r) for r in c.fetchall()]
conn.close()
return {
"total_users": total_users,
"total_applications": total_applications,
"applied": applied,
"interviews": interviews,
"conversion_rate": round(interviews / applied * 100, 1) if applied > 0 else 0,
"api_usage": api_usage,
"plan_distribution": plan_distribution,
"daily_applications": daily_applications,
"top_companies": top_companies
}
@@ -460,9 +621,8 @@ def admin_users():
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("""
SELECT u.id, u.email, u.name, u.created_at,
COUNT(a.id) as applications,
SUM(CASE WHEN a.status = 'interview' THEN 1 ELSE 0 END) as interviews
SELECT u.id, u.email, u.name, u.plan, u.monthly_count, u.ai_customize_count,
COUNT(a.id) as applications
FROM users u
LEFT JOIN applications a ON u.id = a.user_id
GROUP BY u.id
@@ -470,20 +630,39 @@ def admin_users():
""")
rows = c.fetchall()
conn.close()
return [dict(r) for r in rows]
return {"users": [dict(r) for r in rows]}
@router.get("/admin/applications")
def admin_applications():
def admin_applications(limit: int = 100):
"""All applications"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("""
SELECT a.*, u.email, u.name
SELECT a.*, u.email as user_email
FROM applications a
JOIN users u ON a.user_id = u.id
ORDER BY a.applied_at DESC
""")
ORDER BY a.applied_at DESC LIMIT ?
""", (limit,))
rows = c.fetchall()
conn.close()
return [dict(r) for r in rows]
return {"applications": [dict(r) for r in rows]}
@router.post("/admin/reset-usage")
def admin_reset_usage(user_id: str = None):
"""Admin: Reset usage counts (monthly billing cycle reset)"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
if user_id:
c.execute("UPDATE users SET monthly_count = 0, ai_customize_count = 0, billing_cycle_start = CURRENT_TIMESTAMP WHERE id = ?", (user_id,))
msg = f"Reset usage for user {user_id}"
else:
c.execute("UPDATE users SET monthly_count = 0, ai_customize_count = 0, billing_cycle_start = CURRENT_TIMESTAMP")
msg = "Reset all users"
conn.commit()
conn.close()
return {"success": True, "message": msg}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)