Initial commit: AutoJobs MVP - AI job application platform
This commit is contained in:
+489
@@ -0,0 +1,489 @@
|
||||
"""
|
||||
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]
|
||||
Reference in New Issue
Block a user