User routes + some updates
This commit is contained in:
parent
e9d877c0ad
commit
c51440e0f5
@ -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")
|
||||||
|
@ -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
|
@ -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
|
||||||
|
|
||||||
|
@ -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
130
app/routes/user_routes.py
Normal 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
|
11
app/schemas/user_schemas.py
Normal file
11
app/schemas/user_schemas.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class LoginDataSchema(BaseModel):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatePasswordSchema(BaseModel):
|
||||||
|
password: str
|
||||||
|
new_password: str
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user