Files
autojobs/backend/main.py
T

490 lines
15 KiB
Python

"""
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]