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.responses import RedirectResponse
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.token_route import router as token_router
from app.routes.user_routes import router as user_router
from typing import Annotated
from fastapi.exceptions import HTTPException
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
app.include_router(token_router)
app.include_router(refresh_router, prefix="/api")
app.include_router(user_router, prefix="/api")
@app.get("/login")

View File

@ -60,11 +60,7 @@ async def create_link(
except:
continue
return {
"response": "Link successfully created",
"expire_date": new_link.expire_date,
"link": new_link.link,
}
return new_link
@router.delete("/{link}", summary="Delete a link")
@ -94,7 +90,7 @@ async def delete_link(
db.delete(link)
db.commit()
return {"response": "Link successfully deleted", "link": link.link}
return status.HTTP_204_NO_CONTENT
@router.get(
@ -152,4 +148,4 @@ async def delete_link_records(
db.delete(record)
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.responses import RedirectResponse
from datetime import timedelta
from typing import Annotated

View File

@ -44,11 +44,17 @@ async def login_for_access_token(
data={"sub": user.username, "refresh": True},
expires_delta=refresh_token_expires,
)
response = JSONResponse(content={"success": True})
response.set_cookie(
key="access_token", value=access_token, httponly=True, samesite="lax"
# response = JSONResponse(content={"success": True})
# response.set_cookie(
# 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 sqlalchemy.orm import sessionmaker
from app.schemas.auth_schemas import *
from models import User as UserDB
from models import User as UserModel
secret_key = random.randbytes(32)
algorithm = "HS256"
@ -32,7 +32,7 @@ def get_user(db, username: str):
"""
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:
return UserInDB(**user.__dict__)
@ -79,7 +79,8 @@ async def get_current_user_from_cookie(
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)
@ -88,7 +89,8 @@ async def get_current_user_from_token(
# `refresh_get_current_user` is only called from /refresh
# and alerts `get_current_user` that it should expect a refresh token
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)
@ -97,7 +99,7 @@ async def get_current_user(
token: str,
is_refresh: bool = False,
is_ui: bool = False,
db: Optional[sessionmaker] = None,
db: sessionmaker = None,
):
"""
Return the current user based on the token