User routes + some updates

This commit is contained in:
Parker M. 2024-11-04 23:57:29 -06:00
parent e9d877c0ad
commit c51440e0f5
Signed by: parker
GPG Key ID: 505ED36FC12B5D5E
7 changed files with 168 additions and 22 deletions

View File

@ -1,10 +1,11 @@
from fastapi import FastAPI, Path, Depends, Request from fastapi import FastAPI, Depends, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from app.routes.links_route import router as links_router from app.routes.links_routes import router as links_router
from app.routes.refresh_route import router as refresh_router from app.routes.refresh_route import router as refresh_router
from app.routes.token_route import router as token_router from app.routes.token_route import router as token_router
from app.routes.user_routes import router as user_router
from typing import Annotated from typing import Annotated
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from starlette.status import HTTP_404_NOT_FOUND from starlette.status import HTTP_404_NOT_FOUND
@ -41,6 +42,7 @@ app.include_router(links_router, prefix="/api")
# prefix in order to keep logging in via Swagger UI working # prefix in order to keep logging in via Swagger UI working
app.include_router(token_router) app.include_router(token_router)
app.include_router(refresh_router, prefix="/api") app.include_router(refresh_router, prefix="/api")
app.include_router(user_router, prefix="/api")
@app.get("/login") @app.get("/login")

View File

@ -60,11 +60,7 @@ async def create_link(
except: except:
continue continue
return { return new_link
"response": "Link successfully created",
"expire_date": new_link.expire_date,
"link": new_link.link,
}
@router.delete("/{link}", summary="Delete a link") @router.delete("/{link}", summary="Delete a link")
@ -94,7 +90,7 @@ async def delete_link(
db.delete(link) db.delete(link)
db.commit() db.commit()
return {"response": "Link successfully deleted", "link": link.link} return status.HTTP_204_NO_CONTENT
@router.get( @router.get(
@ -152,4 +148,4 @@ async def delete_link_records(
db.delete(record) db.delete(record)
db.commit() db.commit()
return {"response": "Records successfully deleted", "link": link.link} return status.HTTP_204_NO_CONTENT

View File

@ -1,5 +1,4 @@
from fastapi import Depends, APIRouter from fastapi import Depends, APIRouter
from fastapi.responses import RedirectResponse
from datetime import timedelta from datetime import timedelta
from typing import Annotated from typing import Annotated

View File

@ -44,11 +44,17 @@ async def login_for_access_token(
data={"sub": user.username, "refresh": True}, data={"sub": user.username, "refresh": True},
expires_delta=refresh_token_expires, expires_delta=refresh_token_expires,
) )
response = JSONResponse(content={"success": True}) # response = JSONResponse(content={"success": True})
response.set_cookie( # response.set_cookie(
key="access_token", value=access_token, httponly=True, samesite="lax" # key="access_token", value=access_token, httponly=True, samesite="lax"
# )
# response.set_cookie(
# key="refresh_token", value=refresh_token, httponly=True, samesite="lax"
# )
# For Swagger UI to work, must return the token
return Token(
access_token=access_token,
refresh_token=refresh_token,
token_type="bearer",
) )
response.set_cookie(
key="refresh_token", value=refresh_token, httponly=True, samesite="lax"
)
return response

130
app/routes/user_routes.py Normal file
View File

@ -0,0 +1,130 @@
from fastapi import APIRouter, status, Path, Depends
from fastapi.exception_handlers import HTTPException
from typing import Annotated
import string
import bcrypt
import random
import datetime
import validators
from app.util.db_dependency import get_db
from app.schemas.auth_schemas import User
from app.schemas.user_schemas import *
from models import User as UserModel
from app.util.authentication import get_current_user_from_token
router = APIRouter(prefix="/user", tags=["user"])
# In order to help protect some anonymity/privacy, user routes
# do not use path parameters, as then people could potentially
# see if a specific username exists or not. Instead, the user
# routes will use query parameters to specify the user to act
@router.post("/register", summary="Register a new user")
async def get_links(
login_data: LoginDataSchema,
db=Depends(get_db),
):
"""
Given the login data (username, password) process the registration of a new
user account and return either the user or an error message
"""
username = login_data.username
password = login_data.password
# Make sure the password meets all of the requirements
if len(password) < 8:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password must be at least 8 characters",
)
if not any(char.isdigit() for char in password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password must contain at least one digit",
)
if not any(char.isupper() for char in password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password must contain at least one uppercase letter",
)
# Make sure the username isn't taken
user = db.query(UserModel).filter(UserModel.username == username).first()
if user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Username not available",
)
# Otherwise, hash the password, create the api key, and add the new user
hashed_password = bcrypt.hashpw(
password.encode("utf-8"), bcrypt.gensalt()
).decode("utf-8")
api_key = "".join(
random.choices(string.ascii_letters + string.digits, k=20)
)
new_user = UserModel(
username=username, hashed_password=hashed_password, api_key=api_key
)
db.add(new_user)
db.commit()
return status.HTTP_201_CREATED
@router.get("/delete", summary="Delete a user - provided it's your own")
async def delete_user(
current_user: Annotated[User, Depends(get_current_user_from_token)],
db=Depends(get_db),
):
"""
Delete the user account associated with the current user
"""
user = db.query(UserModel).filter(UserModel.id == current_user.id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
db.delete(user)
db.commit()
return status.HTTP_204_NO_CONTENT
@router.put("/updatepass", summary="Update your account's password")
async def update_pass(
current_user: Annotated[User, Depends(get_current_user_from_token)],
update_data: UpdatePasswordSchema,
db=Depends(get_db),
):
"""
Update the pass of the current user account
"""
# Make sure the password meets all of the requirements
if len(update_data.new_password) < 8:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password must be at least 8 characters",
)
if not any(char.isdigit() for char in update_data.new_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password must contain at least one digit",
)
if not any(char.isupper() for char in update_data.new_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password must contain at least one uppercase letter",
)
# Get the user and update the password
user = db.query(UserModel).filter(UserModel.id == current_user.id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
user.hashed_password = bcrypt.hashpw(
update_data.new_password.encode("utf-8"), bcrypt.gensalt()
).decode("utf-8")
db.commit()
return status.HTTP_204_NO_CONTENT

View File

@ -0,0 +1,11 @@
from pydantic import BaseModel
class LoginDataSchema(BaseModel):
username: str
password: str
class UpdatePasswordSchema(BaseModel):
password: str
new_password: str

View File

@ -11,7 +11,7 @@ import jwt
from app.util.db_dependency import get_db from app.util.db_dependency import get_db
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from app.schemas.auth_schemas import * from app.schemas.auth_schemas import *
from models import User as UserDB from models import User as UserModel
secret_key = random.randbytes(32) secret_key = random.randbytes(32)
algorithm = "HS256" algorithm = "HS256"
@ -32,7 +32,7 @@ def get_user(db, username: str):
""" """
Get the user object from the database Get the user object from the database
""" """
user = db.query(UserDB).filter(UserDB.username == username).first() user = db.query(UserModel).filter(UserModel.username == username).first()
if user: if user:
return UserInDB(**user.__dict__) return UserInDB(**user.__dict__)
@ -79,7 +79,8 @@ async def get_current_user_from_cookie(
async def get_current_user_from_token( async def get_current_user_from_token(
token: Annotated[str, Depends(oauth2_scheme)], db=Depends(get_db) token: Annotated[str, Depends(oauth2_scheme)],
db=Depends(get_db),
): ):
return await get_current_user(token, db=db) return await get_current_user(token, db=db)
@ -88,7 +89,8 @@ async def get_current_user_from_token(
# `refresh_get_current_user` is only called from /refresh # `refresh_get_current_user` is only called from /refresh
# and alerts `get_current_user` that it should expect a refresh token # and alerts `get_current_user` that it should expect a refresh token
async def refresh_get_current_user( async def refresh_get_current_user(
token: Annotated[str, Depends(oauth2_scheme)], db=Depends(get_db) token: Annotated[str, Depends(oauth2_scheme)],
db=Depends(get_db),
): ):
return await get_current_user(token, is_refresh=True, db=db) return await get_current_user(token, is_refresh=True, db=db)
@ -97,7 +99,7 @@ async def get_current_user(
token: str, token: str,
is_refresh: bool = False, is_refresh: bool = False,
is_ui: bool = False, is_ui: bool = False,
db: Optional[sessionmaker] = None, db: sessionmaker = None,
): ):
""" """
Return the current user based on the token Return the current user based on the token