""" AutoJobs API — FastAPI Backend Multi-user job search with per-user API keys """ from fastapi import FastAPI, HTTPException, Depends 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 contextlib import asynccontextmanager DB_PATH = os.getenv("DB_PATH", "/var/www/autojobs/autojobs.db") # Router with /autojobs/api prefix router = APIRouter(prefix="/autojobs/api") # --- Database --- def init_db(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute(""" CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, name TEXT, api_keys TEXT DEFAULT '{}', profile TEXT DEFAULT '{}', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) c.execute(""" CREATE TABLE IF NOT EXISTS applications ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, job_title TEXT, company TEXT, location TEXT, job_url TEXT, status TEXT DEFAULT 'pending', resume_version TEXT, cover_letter TEXT, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ) """) c.execute(""" CREATE TABLE IF NOT EXISTS api_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, source TEXT, jobs_found INTEGER, searched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ) """) conn.commit() conn.close() @asynccontextmanager async def lifespan(app: FastAPI): init_db() yield app = FastAPI(title="AutoJobs API", version="1.0.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(router) security = HTTPBearer(auto_error=False) # --- Simple Auth (API Key in header) --- 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"} # --- Models --- class UserProfile(BaseModel): name: str email: str title: str summary: str skills: List[str] experience: List[dict] education: str linkedin_url: str = "" job_keywords: List[str] job_locations: List[str] class APIKeys(BaseModel): jooble: str = "" jsearch: str = "" perplexity: str = "" class JobSearchRequest(BaseModel): query: str location: str = "" page: int = 1 class ApplicationSubmit(BaseModel): job: dict resume: str cover_letter: str status: str = "applied" # --- Routes --- @router.get("/") def root(): return {"status": "ok", "service": "AutoJobs API", "version": "1.0.0"} @router.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""" 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() conn.close() return {"status": "ok", "user_id": user_id} @router.get("/users/me") def get_me(user=Depends(get_current_user)): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row c = conn.cursor() c.execute("SELECT * FROM users WHERE id = ?", (user["id"],)) row = c.fetchone() conn.close() if not row: raise HTTPException(status_code=404, detail="User not found") return dict(row) @router.post("/users/me/api-keys") def save_api_keys(keys: APIKeys, user=Depends(get_current_user)): """Save user's 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"])) conn.commit() conn.close() return {"status": "ok"} @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 "{}") @router.post("/users/me/profile") def save_profile(profile: UserProfile, user=Depends(get_current_user)): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("UPDATE users SET profile = ? WHERE id = ?", (json.dumps(profile.model_dump()), user["id"])) conn.commit() conn.close() return {"status": "ok"} @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"]) # --- 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: 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", "") }) except Exception as e: print(f"Jooble error: {e}") # JSearch (RapidAPI) if jsearch_key: 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', '')}" }) 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 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)} # --- AI Customization --- @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')} ## Summary {user_profile.get('summary', '')} ## Key Skills (matched to job requirements) {', '.join(relevant_skills) if relevant_skills else ', '.join(skills[:8])} ## 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, I am excited to apply for the {job_title} position at {company_name}. I bring {user_profile.get('summary', 'a strong background in software development and AI')} with key skills in {', '.join(skills[:5])}. {"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"}. I would welcome the opportunity to discuss how my background aligns with your needs. Best regards, {user_profile.get('name', 'Applicant')} """ return { "resume": tailored_resume, "cover_letter": cover_letter, "matched_skills": relevant_skills } # --- Applications --- @router.post("/applications") def submit_application(app_data: ApplicationSubmit, user=Depends(get_current_user)): import uuid app_id = str(uuid.uuid4())[:8] 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 (?, ?, ?, ?, ?, ?, ?, ?) """, ( 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_data.status, app_data.cover_letter[:500] if app_data.cover_letter else "" )) conn.commit() conn.close() return {"status": "ok", "application_id": app_id} @router.get("/applications") def get_applications(user=Depends(get_current_user)): 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"],)) rows = c.fetchall() conn.close() return [dict(r) for r in rows] @router.patch("/applications/{app_id}") def update_application(app_id: str, status: str, user=Depends(get_current_user)): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("UPDATE applications SET status = ? WHERE id = ? AND user_id = ?", (status, app_id, user["id"])) conn.commit() conn.close() return {"status": "ok"} # --- Admin Routes --- @router.get("/admin/stats") def admin_stats(): """Global platform 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 """) api_usage = [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 """) 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, "daily_applications": daily_applications, "top_companies": top_companies } @router.get("/admin/users") def admin_users(): """All users with stats""" conn = sqlite3.connect(DB_PATH) 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 FROM users u LEFT JOIN applications a ON u.id = a.user_id GROUP BY u.id ORDER BY u.created_at DESC """) rows = c.fetchall() conn.close() return [dict(r) for r in rows] @router.get("/admin/applications") def admin_applications(): """All applications""" conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row c = conn.cursor() c.execute(""" SELECT a.*, u.email, u.name FROM applications a JOIN users u ON a.user_id = u.id ORDER BY a.applied_at DESC """) rows = c.fetchall() conn.close() return [dict(r) for r in rows]