490 lines
15 KiB
Python
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]
|