From b5c5e7d512faf2910711de82c0617e210d099860 Mon Sep 17 00:00:00 2001 From: Cullen Date: Sun, 9 Jul 2023 18:42:44 -0500 Subject: [PATCH] feat(users): add register route --- api/__init__.py | 2 + api/auth/__init__.py | 8 +++ api/{v1/token/auth.py => auth/auth_utils.py} | 4 +- api/{v1/token => auth}/db_utils.py | 28 +++++++-- api/auth/register/__init__.py | 28 +++++++++ api/{v1 => auth}/token/__init__.py | 9 ++- api/core/users/__init__.py | 10 +++- api/v1/__init__.py | 7 ++- api/v1/jobs/__init__.py | 3 +- main.py | 1 + requirements.txt | 60 ++++++++++++++++---- 11 files changed, 130 insertions(+), 30 deletions(-) create mode 100644 api/auth/__init__.py rename api/{v1/token/auth.py => auth/auth_utils.py} (93%) rename api/{v1/token => auth}/db_utils.py (59%) create mode 100644 api/auth/register/__init__.py rename api/{v1 => auth}/token/__init__.py (80%) diff --git a/api/__init__.py b/api/__init__.py index 02838e3..be2fc6b 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -1,4 +1,5 @@ from fastapi import APIRouter +from api.auth import router as auth_router from .v1 import router as v1_router router = APIRouter( @@ -6,3 +7,4 @@ router = APIRouter( tags=["api"], ) router.include_router(v1_router) +router.include_router(auth_router) diff --git a/api/auth/__init__.py b/api/auth/__init__.py new file mode 100644 index 0000000..269898e --- /dev/null +++ b/api/auth/__init__.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from api.auth.token import router as token_router +from api.auth.register import router as register_router + +router = APIRouter(prefix="/auth") +router.include_router(token_router) +router.include_router(register_router) diff --git a/api/v1/token/auth.py b/api/auth/auth_utils.py similarity index 93% rename from api/v1/token/auth.py rename to api/auth/auth_utils.py index 8cf6fd6..12710a8 100644 --- a/api/v1/token/auth.py +++ b/api/auth/auth_utils.py @@ -6,10 +6,10 @@ from fastapi.security import OAuth2PasswordBearer from settings import * from api.core.users import TokenData -from .db_utils import UserInDB, get_user +from api.auth.db_utils import UserInDB, get_user load_dotenv() -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/token") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token") def create_access_token(data: dict): diff --git a/api/v1/token/db_utils.py b/api/auth/db_utils.py similarity index 59% rename from api/v1/token/db_utils.py rename to api/auth/db_utils.py index 04fe8af..339e540 100644 --- a/api/v1/token/db_utils.py +++ b/api/auth/db_utils.py @@ -1,25 +1,41 @@ from passlib.context import CryptContext - from supabase_py import create_client, Client +from fastapi import HTTPException, status + 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']: +def create_user(user_create: UserInDB): + result = supabase.table("users").insert(user_create.dict()).execute() + print(f"Insert result: {result}") + + if "error" in result and result["error"]: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"User could not be created due to {result['error']['message']}", + ) + + return result + + +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 + if result["data"]: + user_data = result["data"][0] return UserInDB(**user_data) else: return None + def verify_password(password: str, hashed_password: str): return pwd_context.verify(password, hashed_password) diff --git a/api/auth/register/__init__.py b/api/auth/register/__init__.py new file mode 100644 index 0000000..1e140ca --- /dev/null +++ b/api/auth/register/__init__.py @@ -0,0 +1,28 @@ +from fastapi import APIRouter, HTTPException, status +from api.core.users import UserCreate, UserInDB +from api.auth.db_utils import get_user, get_password_hash, create_user + +router = APIRouter(prefix="/register", tags=["register"]) + + +@router.post("/") +async def register_new_user(user: UserCreate): + existing_user = get_user(user.username) + if existing_user is not None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already exists", + ) + + hashed_password = get_password_hash(user.password) + print(f"Hashed password: {hashed_password}") + user_create = UserInDB( + username=user.username, + email=user.email, + full_name=user.full_name, + hashed_password=hashed_password, + disabled=False, + ) + create_user(user_create) + + return {"detail": "User created successfully"} diff --git a/api/v1/token/__init__.py b/api/auth/token/__init__.py similarity index 80% rename from api/v1/token/__init__.py rename to api/auth/token/__init__.py index e3eebf3..9f73f1c 100644 --- a/api/v1/token/__init__.py +++ b/api/auth/token/__init__.py @@ -2,17 +2,16 @@ 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 +from api.auth.db_utils import authenticate_user +from api.auth.auth_utils import create_access_token -ACCESS_TOKEN_EXPIRE_MINUTES = 30 -router = APIRouter(prefix="/token") +router = APIRouter(prefix="/token", tags=["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: + if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", diff --git a/api/core/users/__init__.py b/api/core/users/__init__.py index 5b33b94..55f7e8f 100644 --- a/api/core/users/__init__.py +++ b/api/core/users/__init__.py @@ -1,10 +1,18 @@ from pydantic import BaseModel + class User(BaseModel): username: str full_name: str email: str - disabled: bool + disabled: bool = False + + +class UserCreate(BaseModel): + username: str + full_name: str + email: str + password: str class UserInDB(User): diff --git a/api/v1/__init__.py b/api/v1/__init__.py index 33f258d..63eb1a7 100644 --- a/api/v1/__init__.py +++ b/api/v1/__init__.py @@ -1,7 +1,8 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends from .jobs import router as jobs_router -from .token import router as token_router +from api.auth.token import router as token_router +from api.auth.auth_utils import get_active_current_user -router = APIRouter(prefix="/v1") +router = APIRouter(prefix="/v1", dependencies=[Depends(get_active_current_user)]) router.include_router(jobs_router) router.include_router(token_router) diff --git a/api/v1/jobs/__init__.py b/api/v1/jobs/__init__.py index 5ba1ec9..e79c4bf 100644 --- a/api/v1/jobs/__init__.py +++ b/api/v1/jobs/__init__.py @@ -4,9 +4,8 @@ from api.core.scrapers.indeed import IndeedScraper from api.core.scrapers.ziprecruiter import ZipRecruiterScraper from api.core.scrapers.linkedin import LinkedInScraper from api.core.scrapers import ScraperInput, Site -from ...v1.token.auth import get_active_current_user -router = APIRouter(prefix="/jobs", dependencies=[Depends(get_active_current_user)]) +router = APIRouter(prefix="/jobs") SCRAPER_MAPPING = { Site.LINKEDIN: LinkedInScraper, diff --git a/main.py b/main.py index f87c3d1..14bb377 100644 --- a/main.py +++ b/main.py @@ -10,6 +10,7 @@ app = FastAPI( ) app.include_router(api_router) + @app.get("/", tags=["health"]) async def health_check(): return {"message": "JobSpy ready to scrape"} diff --git a/requirements.txt b/requirements.txt index b48c7e6..a8e0f11 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,49 @@ -fastapi~=0.99.1 -pydantic~=1.10.11 -beautifulsoup4~=4.12.2 -requests -pip~=21.3.1 -wheel~=0.37.1 -setuptools~=60.2.0 -passlib~=1.7.4 -cryptography~=41.0.1 -python-jose~=3.3.0 -python-dotenv~=1.0.0 \ No newline at end of file +anyio==3.7.1 +attrs==23.1.0 +bcrypt==4.0.1 +beautifulsoup4==4.12.2 +certifi==2023.5.7 +cffi==1.15.1 +chardet==4.0.0 +charset-normalizer==3.2.0 +click==8.1.4 +cryptography==41.0.1 +dataclasses==0.6 +deprecation==2.1.0 +ecdsa==0.18.0 +exceptiongroup==1.1.2 +fastapi==0.99.1 +gotrue==0.2.0 +h11==0.14.0 +httpcore==0.12.3 +httpx==0.16.1 +idna==2.10 +iniconfig==2.0.0 +packaging==23.1 +passlib==1.7.4 +pluggy==1.2.0 +postgrest-py==0.4.0 +py==1.11.0 +pyasn1==0.5.0 +pycparser==2.21 +pydantic==1.10.11 +pytest==6.2.5 +python-dateutil==2.8.2 +python-dotenv==1.0.0 +python-jose==3.3.0 +python-multipart==0.0.6 +realtime-py==0.1.3 +requests==2.25.1 +rfc3986==1.5.0 +rsa==4.9 +six==1.16.0 +sniffio==1.3.0 +soupsieve==2.4.1 +starlette==0.27.0 +supabase-py==0.0.2 +tls-client==0.2.1 +toml==0.10.2 +typing_extensions==4.7.1 +urllib3==1.26.16 +uvicorn==0.22.0 +websockets==9.1