feat(auth): add auth to jobs endpoint

pull/12/head
Cullen Watson 2023-07-09 15:15:39 -05:00
parent dd0047a5bb
commit 3b9a491d89
11 changed files with 164 additions and 10 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@
/ven/ /ven/
**/__pycache__/ **/__pycache__/
*.pyc *.pyc
.env

View File

@ -1,5 +1,8 @@
from fastapi import APIRouter from fastapi import APIRouter
from .v1 import router as v1_router from .v1 import router as v1_router
router = APIRouter(prefix="/api", tags=["api"]) router = APIRouter(
prefix="/api",
tags=["api"],
)
router.include_router(v1_router) router.include_router(v1_router)

View File

@ -0,0 +1,20 @@
from pydantic import BaseModel
class User(BaseModel):
username: str
full_name: str
email: str
disabled: bool
class UserInDB(User):
hashed_password: str
class TokenData(BaseModel):
username: str
class Token(BaseModel):
access_token: str
token_type: str

View File

@ -1,5 +1,7 @@
from fastapi import APIRouter from fastapi import APIRouter
from .jobs import router as jobs_router from .jobs import router as jobs_router
from .token import router as token_router
router = APIRouter(prefix="/v1") router = APIRouter(prefix="/v1")
router.include_router(jobs_router) router.include_router(jobs_router)
router.include_router(token_router)

View File

@ -1,11 +1,12 @@
from fastapi import APIRouter from fastapi import APIRouter, Depends
from api.core.scrapers.indeed import IndeedScraper from api.core.scrapers.indeed import IndeedScraper
from api.core.scrapers.ziprecruiter import ZipRecruiterScraper from api.core.scrapers.ziprecruiter import ZipRecruiterScraper
from api.core.scrapers.linkedin import LinkedInScraper from api.core.scrapers.linkedin import LinkedInScraper
from api.core.scrapers import ScraperInput, Site from api.core.scrapers import ScraperInput, Site
from ...v1.token.auth import get_active_current_user
router = APIRouter(prefix="/jobs") router = APIRouter(prefix="/jobs", dependencies=[Depends(get_active_current_user)])
SCRAPER_MAPPING = { SCRAPER_MAPPING = {
Site.LINKEDIN: LinkedInScraper, Site.LINKEDIN: LinkedInScraper,

23
api/v1/token/__init__.py Normal file
View File

@ -0,0 +1,23 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from api.core.users import Token
from .db_utils import authenticate_user
from .auth import create_access_token
ACCESS_TOKEN_EXPIRE_MINUTES = 30
router = APIRouter(prefix="/token")
@router.post("/", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(data={"sub": user.username})
return {"access_token": access_token, "token_type": "bearer"}

50
api/v1/token/auth.py Normal file
View File

@ -0,0 +1,50 @@
from datetime import datetime, timedelta
from jose import jwt, JWTError
from fastapi import HTTPException, status, Depends
from fastapi.security import OAuth2PasswordBearer
from settings import *
from api.core.users import TokenData
from .db_utils import UserInDB, get_user
load_dotenv()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/token")
def create_access_token(data: dict):
print(JWT_SECRET_KEY)
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: str = Depends(oauth2_scheme)):
credential_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credential_exception
token_data = TokenData(username=username)
except JWTError:
raise credential_exception
current_user = get_user(token_data.username)
if current_user is None:
raise credential_exception
return current_user
async def get_active_current_user(current_user: UserInDB = Depends(get_current_user)):
if current_user.disabled:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Inactive user."
)
return current_user

37
api/v1/token/db_utils.py Normal file
View File

@ -0,0 +1,37 @@
from passlib.context import CryptContext
from supabase_py import create_client, Client
from api.core.users import UserInDB
from settings import SUPABASE_URL, SUPABASE_KEY
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
def get_user(username: str):
result = supabase.table('users').select().eq('username', username).execute()
if 'error' in result and result['error']:
print(f"Error: {result['error']['message']}")
return None
else:
if result['data']:
user_data = result['data'][0] # get the first (and should be only) user with the matching username
return UserInDB(**user_data)
else:
return None
def verify_password(password: str, hashed_password: str):
return pwd_context.verify(password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def authenticate_user(username: str, password: str):
user = get_user(username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user

14
main.py
View File

@ -1,11 +1,15 @@
from fastapi import FastAPI from fastapi import FastAPI
from supabase_py import create_client, Client
from api import router as api_router from api import router as api_router
app = FastAPI() app = FastAPI(
title="JobSpy Backend",
description="Endpoints for job board scrapers",
version="1.0.0",
)
app.include_router(api_router) app.include_router(api_router)
@app.get("/", tags=["health"])
@app.get("/") async def health_check():
async def root(): return {"message": "JobSpy ready to scrape"}
return {"message": "JobSpy Backend"}

View File

@ -1,7 +1,11 @@
fastapi~=0.99.1 fastapi~=0.99.1
pydantic~=1.10.11 pydantic~=1.10.11
beautifulsoup4~=4.12.2 beautifulsoup4~=4.12.2
requests~=2.31.0 requests
pip~=21.3.1 pip~=21.3.1
wheel~=0.37.1 wheel~=0.37.1
setuptools~=60.2.0 setuptools~=60.2.0
passlib~=1.7.4
cryptography~=41.0.1
python-jose~=3.3.0
python-dotenv~=1.0.0

9
settings.py Normal file
View File

@ -0,0 +1,9 @@
from dotenv import load_dotenv
import os
load_dotenv()
SUPABASE_URL = os.environ.get("SUPABASE_URL")
SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30